36. Типовые проблемы: симптом → причина → лечение
Каждый раздел справочника заканчивался «граблями» — здесь они собраны вместе и переставлены. Не по темам, а по тому, как выглядит беда: вы видите симптом, а не причину, и искать нужно от него. Больше четырёхсот строк, и большинство приборов, которые «не работают без причины», попадают в одну из них.
Цифра в последнем столбце — раздел, где разобрано подробно.
Как это называется в документации
Оригинал Перевод troubleshooting поиск и устранение неисправностей symptom симптом, внешнее проявление root cause первопричина intermittent перемежающийся, «иногда» heisenbug ошибка, исчезающая при попытке наблюдения works on my desk «на столе работает» sanity check проверка очевидного bisect поиск делением пополам
36.1 Порядок действий
Прежде чем искать по таблице — пять вопросов по порядку. Ответ «нет» на любой из них означает, что дальше искать рано.
1. Собирается ли без предупреждений? Не только без ошибок. Предупреждение о неявном приведении, неиспользуемом значении или неинициализированной переменной — в половине случаев и есть ответ.
2. Доходит ли до main? Точка останова на первой строке. Не дошёл —
неверный файл запуска, таблица векторов, стек (раздел 11).
3. Тактируется ли то, что не работает? Половина всех «код правильный,
а не работает» — забытый бит в RCC->AHBENR/APB1ENR/APB2ENR.
Проверить регистр в отладчике, не код: код мог быть написан, но не дойти.
4. Та ли частота? Прочитать RCC->CFGR и SWS в отладчике, а не верить
SystemCoreClock. Кварц не той частоты, PLL не включилась, SWS не проверен —
и всё, что зависит от времени, врёт пропорционально.
5. Включено ли прерывание на всех уровнях? Периферия → NVIC →
глобально. И приоритет назначен явно — по умолчанию он нулевой, и под
ядром это ошибка.
Дальше — таблица.
Три приёма, которые работают всегда
Делить пополам. Убрать половину кода — заработало? Значит беда в убранной половине. Повторять. Десять шагов — тысяча строк.
Свести к минимуму. Отдельный проект из десяти строк, который воспроизводит беду. Если не воспроизводится — беда во взаимодействии, и это уже ответ.
Читать регистры, не код. Код говорит, что вы хотели. Регистр — что получилось. Расходятся — где-то между ними.
36.2 Не собирается и не компонуется
| Симптом | Причина | Лечение | § |
|---|---|---|---|
у коллеги file not found на заголовке своего же проекта |
в Include Paths абсолютный путь | пути от файла проекта: .\App;.\Board;.\Drivers |
4а |
undefined symbol при компоновке, хотя файл в папке есть |
файл не добавлен в группу проекта µVision | добавить файл в группу | 4а |
Error #540: … component is not available … pack … is not selected |
версии паков не закреплены, а самый новый пак устроен иначе | закрепить версии в Select Software Packs | 4а |
| «uses RTOS support that is part of a Legacy Pack» | в цели чужого проекта включена старая ОС RTX | в настройках цели выбрать Operating system: None | 4а |
SVC_Handler определён дважды |
заготовки обработчиков остались в файле прерываний | удалить три заготовки; подмену имён в конфигурации оставить | 24 |
| неразрешённая ссылка на функцию ядра, которая есть в исходниках | не включён её INCLUDE_ |
добавить макрос в FreeRTOSConfig.h |
24, 33 |
неразрешённая ссылка на vApplicationGetIdleTaskMemory |
включено статическое размещение | написать обе функции выдачи памяти | 24 |
#error из FreeRTOS.h |
обязательный макрос не задан | сообщение называет его | 33 |
ошибка на pvPortMalloc |
не подключён файл кучи | добавить один heap_N.c |
25 |
ошибка на xPortGetMinimumEverFreeHeapSize |
подключён heap_1/2/3 |
перейти на heap_4 |
25 |
__forceinline не компилируется |
адаптация RVDS под Arm Compiler 6 |
взять portable/GCC/ARM_CM3 |
24 |
| программные таймеры не собираются | нет INCLUDE_xTaskGetSchedulerState или configTIMER_* |
добавить | 33 |
| прошивка внезапно потяжелела | включены таймеры, группы, буферы — а нужно одно | отключить лишнее | 24 |
'else' without a previous 'if' после вызова макроса |
макрос из нескольких операторов без do { } while (0) |
обернуть тело в do { … } while (0) |
127 |
| после перехода на C23 — «too many arguments to function call» | объявление f() без void: в C23 это функция без параметров |
написать прототип с типами параметров | 128 |
| «unknown type name 'constexpr'» | стандарт ниже C23 или компилятор без C23 | -std=c23 на armclang 6.24 или #define / enum |
128 |
не найден __stack_chk_fail при компоновке |
включена -fstack-protector-strong, а обработчика и контрольного слова нет |
написать __stack_chk_guard и __stack_chk_fail |
129 |
код с F4 на F0 или G0 не собирается: неизвестна __set_BASEPRI |
у Cortex-M0 и M0+ нет BASEPRI |
критическая секция через PRIMASK (__disable_irq) |
7 |
| на F0 после своего загрузчика прерывания уходят в его таблицу | у Cortex-M0 нет VTOR |
скопировать таблицу в начало ОЗУ и включить MEM_MODE в SYSCFG->CFGR1 |
7 |
undefined reference, хотя -l указан |
библиотека в командной строке раньше объекта, который её использует | -l после исходников и объектов |
143 |
relocation R_X86_64_32 … recompile with -fPIC |
объект для .so собран без -fPIC |
пересобрать с -fPIC |
143 |
libpq-fe.h: No such file |
заголовки libpq лежат в /usr/include/postgresql |
-I$(pg_config --includedir) |
148 |
insmod: Invalid module format |
vermagic модуля не совпал с ядром |
собрать под заголовки своего ядра | 147 |
class_create не собирается |
с ядра 6.4 один аргумент, раньше два | ветка по LINUX_VERSION_CODE |
147 |
fatal error: esp_flash.h: No such file в проекте ESP-IDF |
у компонента указан REQUIRES — зависимости теперь только явные |
дописать компонент в REQUIRES |
151 |
ledc_timer_config вернул ESP_FAIL |
частота × 2^разрядность больше 80 МГц | уменьшить разрядность или частоту | 153 |
| прошивка собралась, а образ 0 байт и плата молчит | в своём скрипте компоновщика нет KEEP(*(.isr_vector)) и ENTRY, а --gc-sections выбросил всё |
вернуть ENTRY(Reset_Handler) и KEEP, после сборки смотреть --print-memory-usage |
4в |
36.3 Не запускается
| Симптом | Причина | Лечение | § |
|---|---|---|---|
не доходит до main |
файл запуска, таблица векторов, указатель стека | точка останова в Reset_Handler |
11 |
HardFault сразу после vTaskStartScheduler |
имена трёх обработчиков не подменены | макросы vPortSVCHandler и др. |
24 |
| собирается, задачи не переключаются | то же: вектора ведут на пустые заготовки | то же | 24 |
останов внутри port.c при старте |
configASSERT: приоритет прерывания или группировка |
стек вызовов; PRIGROUP ≤ 3 |
24, 28 |
стоит в vPortValidateInterruptPriority |
прерывание выше порога вызвало функцию ядра | приоритет ≥ 5 или убрать вызов | 28 |
| останов сразу после старта задачи | задача вышла из своей функции | бесконечный цикл или vTaskDelete(NULL) |
26 |
| памяти не хватает с первого запуска | куча 10 КБ на камне с 20 | уменьшить или статическое размещение | 25 |
| часы «сбрасываются» при каждой перезагрузке | RTC инициализируется безусловно | проверять флаг инициализации в backup | 12 |
| мгновенный выход из Standby | не сброшен CWUF |
сбросить перед входом | 10 |
cannot open shared object file при запуске |
библиотеки нет в RUNPATH, LD_LIBRARY_PATH и кэше; текущий каталог не ищется |
rpath $ORIGIN или установить в систему |
143 |
insmod: Operation not permitted |
нет прав или контейнер | root на настоящей машине |
147 |
| ESP32 не стартует с подключённой периферией | датчик тянет strapping-вывод при сбросе; GPIO12 вверх переводит флеш на 1,8 В | освободить выводы 0, 2, 5, 12, 15 на время сброса | 153 |
| ESP32 перезагружается при включении Wi-Fi | просадка ниже порога 2,43 В: слабый стабилизатор или кабель | питание с запасом на пики радио | 150 |
| модуль на 4 МБ, а программа видит 2 | размер флеш-памяти в menuconfig по умолчанию 2 МБ |
CONFIG_ESPTOOLPY_FLASHSIZE_4MB=y |
150 |
| ESP32 после обновления по сети снова на старой версии | новая прошивка перезапустилась до подтверждения — загрузчик откатил её | подтверждать esp_ota_mark_app_valid_cancel_rollback() после самопроверки |
159 |
| после прошивки программатором ESP32 грузит старое приложение | в otadata осталась запись о разделе ota_0 |
стереть otadata или всю флеш-память |
159 |
36.4 Периферия молчит
| Симптом | Причина | Лечение | § |
|---|---|---|---|
код АЦП с F1 на F0 или G0 не собирается: нет SQR3, CR2 |
у АЦП F0/G0 нет очереди — каналы отмечаются битами | маска в CHSELR, запуск ADSTART, конец EOC |
14 |
G0: АЦП не даёт ADRDY, калибровка не заканчивается |
не включён регулятор ADVREGEN или не выждано 20 мкс после него |
ADVREGEN, пауза 20 мкс, затем ADCAL и ADEN |
14 |
| G0: после смены канала первое измерение — с прежнего канала | новая маска CHSELR вступает не сразу |
после записи CHSELR дождаться CCRDY |
14 |
G0: питание по VREFINT завышено на 10 % |
заводской код снят при 3,0 В, а в формуле 3300 | VREFINT_CAL_VREF из LL своей серии |
14 |
код I²C с F1 на F0 или G0 не собирается: нет SR1, DR, CCR |
у F0/G0 другой блок I²C | TIMINGR, обмен через CR2 (NBYTES, AUTOEND), флаги в ISR |
17 |
F0/G0: программа виснет в ожидании TXIS |
на адрес никто не ответил — пришёл NACKF |
ждать TXIS и NACKF вместе, NACKF сбросить в ICR |
17 |
| F0/G0: после первого обмена блок «занят» | не сброшен STOPF после AUTOEND |
записать STOPCF в ICR |
17 |
код SPI с F1 на F0 или G0 не собирается: нет DFF |
разрядность у нового SPI — поле DS в CR2 |
DS = размер − 1, для 8 бит 0111 |
18 |
| F0/G0: SPI шлёт лишний нулевой байт, приём сдвинут на байт | 16-битная запись в DR при 8-битных посылках |
писать и читать DR байтом: *(volatile uint8_t *)&SPI1->DR |
18 |
F0/G0: RXNE не поднимается после одного байта |
порог очереди приёма — два байта | бит FRXTH в CR2 |
18 |
| G0: канал DMA настроен и включён, а передачи нет | запрос не подведён к каналу — не записан DMAMUX | DMAMUX1_ChannelN->CCR = номер запроса из LL; канал N+1 DMA ↔ канал N DMAMUX |
9 |
код DMA с F1 на F0 или G0 не собирается: DMA_CCR4_DIR, DMA1EN |
биты CCR без номера канала, у F0 бит тактирования DMAEN |
DMA_CCR_DIR, RCC_AHBENR_DMAEN |
9 |
| F0: DMA от USART1 идёт не на тот канал | привязка не как у F1: USART1_TX — канал 2, с USART1TX_DMA_RMP — 4 |
таблица привязок F0 или бит переноса в SYSCFG->CFGR1 |
9 |
код сна с F1/F0 на G0 не собирается: нет PWR->CR, PDDS |
у G0 режим — поле LPMS в CR1, флаги — в SCR |
LL_PWR_MODE_* в LPMS, CWUF1/CSBF в SCR, EWUP1 в CR3 |
10 |
G0: запись во Flash даёт SIZERR или PGAERR |
SIZERR — запись байтом или полусловом; PGAERR — два слова не из одного двойного слова |
двойными словами по адресу, кратному 8: два 32-битных слова подряд | 11 |
код Flash с F1/F0 на G0 не собирается: нет FLASH->AR |
страница у G0 стирается по номеру | поле PNB в CR |
11 |
код таймера с F1/F0 на G0 не собирается: нет RCC->APB1ENR |
у G0 регистры тактирования — APBENR1/APBENR2 |
RCC_APBENR1_TIM3EN, RCC_APBENR2_TIM1EN |
13 |
| F0/G0: частота ШИМ ниже расчётной — в 1,5 раза (F0) или в 1,125 раза (G0); в 9 или 4,5 раза, если система осталась на HSI | PSC рассчитан под 72 МГц F1 |
такт таймера 48 (F0) или 64 МГц (G0): PSC 47 или 63 для счёта 1 МГц; проверить, что система ушла на PLL |
13 |
| код правильный, регистр не меняется | не включён такт блока | бит в RCC->xxxENR; проверить в отладчике |
6 |
HardFault с PRECISERR, BFAR = адрес периферии |
то же: обращение к нетактируемому блоку | то же | 6, 35 |
запись в PWR->CR не проходит |
PWR на APB1 без такта |
RCC_APB1ENR_PWREN |
10 |
| таймер считает, а на выводе ничего | вывод не в альтернативной функции | настроить GPIO |
13 |
| настройки таймера не вступают в силу | нет события обновления | бит UG или дождаться переполнения |
13 |
TIM1 молчит |
не выставлен MOE в BDTR |
выставить | 13 |
| прерывание таймера не приходит | HAL_TIM_Base_Start только запускает счёт, UIE не выставлен |
HAL_TIM_Base_Start_IT |
13 |
| светодиод BluePill не откликается на ШИМ | PC13 к таймеру не подключён |
внешний светодиод на выводе канала, например PA6 |
13 |
| таблица через DMA в ШИМ не идёт | в таймере темпа не разрешён запрос UDE |
DIER = TIM_DIER_UDE |
9 |
| DMA берёт каждое второе значение таблицы | MSIZE 32 бита при таблице uint16_t |
размеры по элементу и регистру | 9 |
| прерывание не приходит | цепочка: такт порта → EXTI → NVIC → глобально | проверять по цепочке | 7 |
| обработчик зовётся бесконечно | не сброшен флаг EXTI->PR |
записать единицу | 7 |
| I²C не работает вовсе | нет подтяжек | 4,7 кОм на обе линии | 17 |
| I²C: конфликт с ведомым | выводы push-pull | открытый сток | 17 |
| SPI не отвечает | режим CPOL/CPHA, MOSI/MISO, CS, скорость |
проверять по порядку | 18 |
| SPI выходит из режима ведущего | не выставлен SSI |
выставить с SSM |
18 |
| CAN не принимает при работающей передаче | не настроен фильтр | хотя бы один принимающий всё | 19 |
| одинокий узел CAN без конца повторяет кадр | некому подтвердить: узел пассивный, но не в bus-off | второй узел или петля | 19 |
| USB не определяется | подтяжка D+, 48 МГц, HSI вместо кварца | резистор 1,5 кОм; кварц; частота | 20 |
| CAN и USB вместе на F103 | общая память буферов | одно из двух | 19, 20 |
| SWO молчит | SWD не JTAG; TRACE_IOEN; TRCENA; ключ; канал; настройки отладчика |
по порядку | 21 |
| SWO не работает на клоне отладчика | клон не принимает трассировку | другой отладчик | 21 |
PB3 не работает как GPIO |
занят под SWO | одно из двух | 21 |
| DMA не стартует | забыто разрешение со стороны периферии (DMAT, DMA в ADC_CR2) |
выставить | 9 |
| DMA на F4 не стартует повторно | остались флаги прошлой передачи | сбросить в IFCR |
9 |
| ЦАП нет в кристалле | его там нет | смотреть даташит до разводки | 15 |
| сторожевой таймер срабатывает при отладке | не выставлен DBGMCU->CR |
остановить сторож в отладке | 12 |
| флеш по SPI пишется «вхолостую» | забыт Write Enable |
перед каждой записью и стиранием | 41 |
f_mount → FR_NO_FILESYSTEM |
носитель не отформатирован | f_mkfs |
42 |
gpio_set_direction вернул ESP_ERR_INVALID_ARG |
выводы 34–39 только на вход, номеров 24 и 28–31 нет | взять другой вывод | 153 |
ADC2 отвечает ESP_ERR_TIMEOUT |
при включённом Wi-Fi второй блок АЦП занят радио | перенести вход на ADC1 (GPIO32–39) | 154 |
adc_continuous_config → ESP_ERR_INVALID_ARG |
частота ниже 20 кГц или выбран ADC2 | 20 кГц … 2 МГц и только ADC1 | 154 |
36.5 Работает, но не так
| Симптом | Причина | Лечение | § |
|---|---|---|---|
| все задержки не те, но пропорционально | configCPU_CLOCK_HZ ≠ реальная частота |
привести к RCC | 33 |
| частота таймера вдвое больше | удвоение такта на APB1 при делителе ≠ 1 | учесть ×2 | 13 |
| мигает вдвое медленнее расчёта | переключение раз в 0,5 с — это период 1 с | период мигания = два переключения | 13 |
| яркость прыгает на максимум при повороте энкодера влево | CNT взят как положение и перешёл через ноль |
разность показаний со знаком, накопитель с ограничением | 13 |
| частота ЦАП/сигнала не та | то же удвоение | то же | 15 |
| задержки стали короче после смены тика | задержки записаны числами | pdMS_TO_TICKS везде |
24 |
| период задачи больше заданного | vTaskDelay считает от вызова |
xTaskDelayUntil |
26 |
| две задачи с разными приоритетами ведут себя как равные | приоритет выше максимума молча ограничен | ≤ configMAX_PRIORITIES − 1 |
26 |
| запас стека кажется вчетверо меньше | функция возвращает слова | ×4 | 26 |
vTaskList показывает приостановленной ждущую задачу |
вечное ожидание уведомления отчёт принимает за приостановку | это норма; eTaskGetState вернёт eBlocked |
26 |
| счётчик считает меньше | гонка на инкременте | критическая секция | 30 |
мьютекс: Give возвращает отказ |
мьютекс не был взят; у рекурсивного — отдаёт не владелец | каждому Give — свой Take в той же задаче |
30 |
| остановленный таймер пошёл сам | xTimerChangePeriod запускает |
xTimerStop следом |
29 |
| выход по маске не гаснет, когда условие пропало | ждали биты группы, а сброс бита группа не сообщает | ждать признак изменения, правила считать самим | 31 |
| выходы считаются по «старым» входам | группу перечитали после пробуждения | разбирать значение, которое вернула xEventGroupWaitBits |
31 |
| доли задач в статистике времени скачут | счётчик тикает реже переключений задач | DWT->CYCCNT или быстрый таймер |
26 |
| камень встаёт при создании задачи | configASSERT на приоритете не меньше configMAX_PRIORITIES |
приоритет меньше максимума | 26 |
| прибор с зависшей задачей не перезагружается | сторож кормит программный таймер без проверки: демон жив всегда | кормить только при отметках всех задач | 29, 12 |
| задача опроса кнопки «залипает» | xQueueSend ждёт места в полной очереди |
ждать 0 и считать потери | 27 |
| одно нажатие даёт несколько событий | дребезг контакта | три одинаковых чтения подряд | 27, 8 |
| таймер срабатывает с опозданием | приоритет демона ниже занятой задачи | демон на максимум | 29 |
| точнее миллисекунды не выходит | разрешение — тик | аппаратный таймер | 29 |
| задача просыпается по кругу без события | флаг группы не сброшен на выходе | clearOnExit = pdTRUE |
31 |
| флаг из прерывания ставится с задержкой | через демона таймеров | включить таймеры, поднять демона | 31 |
| бит 24 группы событий не работает | старший байт занят ядром | биты 0…23 | 31 |
| задача просыпается на каждый байт | триггер 0 или 1 | разумный порог | 32 |
| АЦП врёт после разгона | превышена ADCCLK |
делитель | 14 |
| показания АЦП занижены и плавают | мало времени выборки | увеличить SMPR |
14 |
| АЦП: читается только первый канал | сканирование без DMA | включить DMA | 14 |
| АЦП: погрешность на F1 | нет калибровки | CAL после включения |
14 |
| ЦАП не доходит до краёв | выходной буфер включён | это норма; либо выключить | 15 |
| ЦАП проседает под нагрузкой | буфер выключен, нагрузка низкоомная | включить буфер | 15 |
| сигнал ЦАП ступенчатый | мало точек или нет фильтра | таблица + RC | 15 |
| UART: мусор вместо текста | скорость или делитель | пересчитать BRR |
16 |
| SWO: мусор вместо текста | частота ядра в отладчике ≠ реальной | исправить в настройках | 21 |
| SWO теряет символы | скорость мала для объёма | поднять скорость, меньше вывода | 21 |
| SPI: сдвиг на бит | неверная фаза | CPHA |
18 |
| CAN: скорость не та | поля тайминга не уменьшены на единицу | −1 в каждом поле |
19 |
| CRC продолжает предыдущий расчёт | не сброшен | CRC_CR_RESET |
12 |
| таймаут сторожа впритык | LSI гуляет на десятки процентов | двойной запас | 12 |
| DS18B20 отдаёт ровно +85 °C | прочли до конца измерения или датчик перезапустился по питанию | ждать полное время измерения; 85,000 — перемерить | 96 |
| мороз превращается в тысячи градусов | код DS18B20 прочитан как беззнаковый | приводить к int16_t |
96 |
| DHT22 молчит | короткий стартовый импульс или опрос чаще раза в 2 с | 2 мс (DHT11 — 18 мс), не чаще раза в 2 с | 97 |
| DHT22 на морозе показывает тысячи градусов | знак и модуль прочитаны как дополнительный код | старший бит — знак | 97 |
| CRC SHT31 не сходится никогда | взята функция CRC от 1-Wire | полином 0x31, начало 0xFF, старшим битом вперёд | 97 |
| давление «ни о чём» при верной схеме | калибровка BMP280 не прочитана или разобрана не тем порядком байт | 24 байта с 0x88, младший первым | 98 |
| BME280 не даёт влажность | на модуле BMP280: в регистре 0xD0 — 0x58 | проверять код модели до работы | 98 |
| высота по давлению плывёт на сотни метров | давление уровня моря меняется с погодой: 30 гПа — 245,1 м | p0 от метеослужбы или считать от старта | 98 |
| MPU-6050 отдаёт одни нули | после включения он спит | записать 0 в PWR_MGMT_1 (0x6B) |
99 |
| показания ADXL345 — мусор при верной схеме | разобраны старшим байтом вперёд, как у MPU-6050 | у ADXL345 младший байт первым | 99 |
| угол по гироскопу уходит на десятки градусов за минуту | интегрируется смещение нуля | калибровать ноль в покое, корректировать акселерометром | 99 |
| расстояние HC-SR04 на морозе завышено на проценты | не учтена температура: метр при 0 °C читается как 1037 мм | температура в перевод эха | 100 |
| таймаут HC-SR04 срабатывает на каждом замере | без URS сброс счётчика по фронту эха поднимает флаг обновления |
TIM_CR1_URS |
100 |
| HC-SR501 не видит сидящего человека | датчик ловит движение, а не присутствие | учитывать выдержку или другой датчик | 100 |
| INA219 показывает ток и мощность ноль | не записан калибровочный регистр: после включения он пуст | записать CAL при запуске | 101 |
| ACS712 показывает ток без нагрузки | ноль взят половиной шкалы, а питание датчика не ровно 5 В: при 4,9 В — −268 мА | мерить ноль без нагрузки | 101 |
| показание INA219 «замерло» при большом токе | на шунте больше ±320 мВ — регистр шунта упёрся в ±32 000; бит OVF может не встать | проверять регистр шунта; меньше шунт или другое усиление | 101 |
| пустые весы показывают 39 кг | 24-битный код HX711 без расширения знака: −1 стал 16 777 215 | расширять знак | 102 |
| после смены усиления HX711 первый отсчёт «не тот» | усиление действует со следующего отсчёта | первый отсчёт выбросить | 102 |
| весы изредка показывают дикие значения | прерывание растянуло единицу на SCK дольше 60 мкс — HX711 уснул | запрещать прерывания на пачку импульсов | 102 |
| фоторезистор показывает 0 или 4095 | не свет, а обрыв или замыкание в делителе | проверить делитель; крайние коды — неисправность | 103 |
| BH1750 на солнце застыл на 54 612 лк | код упёрся в 65535 при MTreg 69 | MTreg 31 | 103 |
| сопротивление слоя MQ на треть больше | делитель к АЦП стоит параллельно RL: 7,5 кОм вместо 10 | считать с RL, нагруженным делителем | 103 |
| −250 °C с MAX31855 читается как 3846 °C | 14-битный код без расширения знака | расширять знак | 104 |
| показания MAX6675 не меняются | CS опускают чаще, чем раз в 0,22 с: чтение прерывает измерение | читать раз в 250 мс | 104 |
| термопара в печи показывает 1184 вместо 1200 °C | линейный пересчёт MAX31855 по 41,276 мкВ/°C | поправка по таблице типа K | 104 |
| кадры Ethernet уходят, но никто не отвечает | тип записан младшим байтом вперёд: 0x0608 вместо 0x0806 | числа в полях кадра — старшим байтом вперёд | 105 |
| прибор видит соседей, но не интернет | нет шлюза или маска шире настоящей: чужой адрес ищут ARP-ом | маска и шлюз как у сети | 105 |
| ARP отвечает, а ping молчит | неверная сумма IP или ICMP — получатель выбрасывает пакет молча | считать сумму при нуле в её поле | 105 |
| соединение TCP «живо» через 15 минут после обрыва кабеля | повторы с удвоением: 924,6 с до отказа | своя проверка связи | 106 |
| прибор за NAT перестал получать команды | запись NAT истекла: у UDP — после 30 с тишины | поддержка связи чаще тайм-аута | 106 |
| TCP упирается в сотни килобит на быстром канале | маленькое окно: 2048 байт при 50 мс — 0,33 Мбит/с | окно больше или время ответа меньше | 106 |
| прибор не получает адрес по DHCP | не выставлен флаг «отвечать всем»: ответ ушёл на адрес, которого у прибора ещё нет | флаг 0x8000 в DHCPDISCOVER | 107 |
| прибор ходит на старый адрес сервера | ответ DNS держат в кэше дольше срока из записи | кэш — не дольше TTL | 107 |
| TLS не проходит проверку сертификата | часы прибора стоят на 2000 году: сертификат «ещё не действителен» | синхронизировать часы до TLS | 107 |
| ответ из другой сети не приходит, хотя запрос маршрутизатор пропустил | у получателя нет обратного маршрута: ответ ушёл его шлюзу по умолчанию | маршрут обратно через тот же маршрутизатор | 108 |
| через VPN ходят мелкие пакеты, а большие пропадают | MTU туннеля 1420: 1500-байтный пакет в него не влезает | MTU 1420, MSS 1380 | 108 |
| Wi-Fi соседа на канале 3 мешает сильнее, чем на канале 1 | соседние каналы перекрываются: шаг 5 МГц при ширине 20 | каналы 1, 6 или 11 | 108 |
| прошивка на Ada: «cannot find entry symbol _start», образ на 0x8000 | общая среда light без карты памяти камня | свои crt0.S и link.ld или среда под плату | 113 |
| сборка Ada падает на предупреждениях о Last_Chance_Handler | -gnatwe превращает напоминания о проверках в ошибки | проверки оставить, убрать -gnatw.X | 113 |
| gnatprove без предусловия «доказал» всё | взял старые результаты из того же каталога | прогон в чистой копии | 113 |
| таблица векторов Rust — килобайт на пустом месте | нет крейта камня: cortex-m-rt резервирует все 256 слов | крейт камня с его прерываниями | 114 |
| Cargo собрал программу, но не для камня | забыли --target | --target thumbv7m-none-eabi или thumbv7em-none-eabihf | 114 |
| на другой машине крейт собрался иначе | не закоммичен Cargo.lock | Cargo.lock в репозиторий | 114 |
| PlatformIO: вчера собиралось, сегодня нет | платформа не закреплена и обновилась | ststm32@20.0.0 в platformio.ini | 115 |
| CMake падает на проверке компилятора для голого камня | пробная сборка пытается скомпоновать программу | CMAKE_TRY_COMPILE_TARGET_TYPE = STATIC_LIBRARY | 115 |
| компилятор PlatformIO оказался старым | тулчейн по умолчанию у ststm32 20.0.0 — GCC 7.2.1 | свой пакет в platform_packages | 115 |
| модель на плате отвечает не так, как на компьютере | признаки не приведены к масштабу обучения | среднее и разброс — в прошивку вместе с весами | 116 |
| k-NN не помещается в ОЗУ | хранит всю обучающую выборку: 600 примеров — 4800 байт признаков | порог, дерево или маленькая сеть | 116 |
| на обучающей выборке 100 %, на проверочной хуже порога | модель запомнила примеры | точность — только на данных, которых модель не видела | 116 |
| в спектре вибрации пик, которого в механизме нет | наложение: 160 Гц при выборке 256 Гц видны как 96 Гц | частота выборки с запасом и фильтр перед АЦП | 117 |
| прошивка выросла на 77 КБ от одного БПФ | arm_rfft_fast_init_f32 тянет таблицы всех размеров | arm_rfft_fast_init_256_f32 — под свой размер | 117 |
| подшипник то распознаётся, то нет | признак «частота главного пика»: сильный тон вращения перекрывает дефект | энергия в полосе частот дефекта | 117 |
| модель верна в Python на 83 %, на объекте намного хуже | окна одной записи попали и в обучение, и в проверку | разбивать по записям: честная оценка 75,9 % | 118 |
| дефектов почти не находит при высокой общей точности | редкий класс: 21 окно из 861, модели выгодно его не замечать | веса классов: 180 из 180 вместо 60 | 118 |
| исправный мотор модель считает дисбалансом | у крепления датчика своя чувствительность | калибровка при установке: 410 → 481 из 540 | 118 |
| выходы int8 на плате на единицу расходятся с TensorFlow Lite | CMSIS-NN по умолчанию округляет при пересчёте дважды: совпали лишь 533 из 540 окон | CMSIS_NN_USE_SINGLE_ROUNDING — все 540 бит в бит | 119 |
| после квантования модель отвечает одним классом | вход квантован не масштабом и нулевой точкой входного тензора или без нормировки | масштаб и нулевая точка из .tflite, нормировка как при обучении | 119 |
| сильный сигнал неотличим от очень сильного | насыщение: представительная выборка не видела таких амплитуд | все классы и режимы в представительной выборке | 119 |
| модель нормы не видит усилившуюся вибрацию | общая норма включает все моторы: дисбаланс при пороге 1 % найден в 45 окнах из 400 | своя норма прибора после установки — 400 из 400 | 120 |
| ложных тревог больше, чем обещал порог | порог выбран на других моторах: 49 из 1200 вместо 1 % | своя норма или порог по первым часам работы | 120 |
| прибор привык к растущему дефекту | база подтягивается к новым окнам | переучивать базу только по команде, хранить историю оценок | 120 |
| оценка заряда уходит на старой батарее | фильтр с паспортным R0 списывает лишнюю просадку на заряд: 5,6 % | R0 в состоянии фильтра: 0,7 % | 121 |
| добавили в фильтр состояние — стало хуже | смещение тока плохо наблюдаемо: оценено 0,085 А вместо 0,05 | не добавлять состояний, которых не видит измерение | 121 |
| заряд по напряжению под нагрузкой врёт на десятки процентов | измерено напряжение с просадкой I·R0 и RC-цепочки | таблицу — только после покоя, под нагрузкой — фильтр | 121 |
| в версии выпущенной прошивки -dirty | собрано из несохранённых правок, повторить сборку нельзя | выпуск только из чистого дерева, версия из git describe | 109 |
| прошивка 1.9.3 считается новее 1.10.0 | версии сравниваются как текст | сравнивать по числам: major, minor, patch, коммиты после тега | 109 |
| неизвестно, какой коммит сломал сборку | перебор двадцати коммитов вручную | git bisect run: коммит 13 из 21 за 5 проверок | 109 |
| образ прошивки каждый раз другой | внутри время сборки, пути каталога и идентификатор сборки | SOURCE_DATE_EPOCH, -ffile-prefix-map, --build-id=none — тот же образ байт в байт | 110 |
| тест упал, а выпуск всё равно уехал | конвейер не проверяет код возврата | этап падает на любой непрошедшей проверке, артефакт не создаётся | 110 |
| сборка вдруг изменилась без правок в коде | компилятор «последней версии» обновился сам | версия компилятора закреплена в образе сборки | 110 |
| тайм-аут срабатывает раньше времени после долгой работы | сравнение с start + timeout, который переполнился: через 8 тиков вместо 100 | (uint32_t)(now - start) >= timeout | 122 |
| проверка ~x == 0xF0 для uint8_t всегда ложна | продвижение до int: ~0x0F — это -16 | (uint8_t)~x == 0xF0 | 122 |
| на компьютере сравнение верно, на плате — нет | разная модель данных: (long)-1 < 1u даёт 1 на x86-64 и 0 на Cortex-M | типы из stdint.h, явные приведения, _Static_assert | 122 |
| работает с -O0, ломается с -O2 | неопределённое поведение: переполнение знакового, строгий псевдоним | сборка с ловушками UB и -Wall; биты через memcpy | 123 |
| проверка переполнения исчезла в выпускной сборке | проверка после операции: x + 1 > x компилятор заменил на 1 | проверять до операции или __builtin_add_overflow | 123 |
| HardFault на команде UDF в отладочной прошивке | сработала ловушка неопределённого поведения | адрес в кадре исключения — строка с ошибкой | 123 |
| цикл ожидания флага из прерывания не ждёт или висит | флаг без volatile: armclang превратил ожидание в BX lr | volatile на флаге | 124 |
| счётчик событий из прерывания иногда отстаёт | events++ — LDR, ADDS, STR, прерывание между ними | atomic_fetch_add (LDREX/STREX) или критическая секция | 124 |
| таблицы съели ОЗУ | без const таблица копируется из флеш-памяти в .data | const — таблица остаётся во флеш-памяти | 124 |
| HardFault при вызове обработчика из таблицы | нулевой указатель или таблица в ОЗУ испорчена записью за границу | проверка индекса и указателя, таблица с const — во флеш-памяти | 125 |
| команда из таблицы не находится, хотя есть | таблица для bsearch не отсортирована по имени | тест порядка таблицы на компьютере | 125 |
| HardFault на Cortex-M0 при разборе пакета | (uint32_t )(buf + 1) — невыровненный доступ | memcpy или побайтный разбор: компилятор сам выберет LDRB | 126 |
| получатель видит мусор в пакете | отправлена структура с байтами заполнения | сериализовать поля побайтно | 126 |
| числа от датчика перевёрнуты | датчик отдаёт старшим байтом вперёд, Cortex-M хранит младшим | побайтный разбор с явным порядком или REV | 126 |
| сообщение об ошибке — от соседнего кода | перечисление и таблица текстов написаны порознь | X-макрос из одного списка или _Static_assert на длину таблицы |
127 |
| мусор в терминале после смены частоты шины | делитель UART не пересчитан, ошибка скорости больше 2 % | проверка делителя в #if с #error при сборке |
127 |
| задача портит чужую память при длинном пакете | массив переменной длины на стеке: размер взят из пакета | -Wvla, массив наибольшего размера с проверкой длины |
128 |
| сбой после длинного пакета, возврат из функции в никуда | memcpy в локальный буфер без проверки длины затёр кадр стека |
проверка длины до копирования, -fstack-protector-strong |
129 |
| автомат состояний застыл после сбоя | испорченная переменная состояния, у switch нет default |
default — переход в безопасное состояние |
129 |
| на G0 прерывание по спаду зациклилось | флаг сбрасывают в PR, как у F4, а у G0 спад отмечается в FPR1, фронт — в RPR1 |
сбрасывать записью единицы в FPR1 или RPR1 |
7 |
на G0 кнопка всегда «нажата», IDR читает ноль |
после сброса выводы G0 аналоговые (MODER = 11), цифровой вход выключен |
перевести вывод во вход: MODER = 00 |
8 |
код для F0 не собирается на G0: неизвестна GPIO_MODER_MODER5 |
заголовок G0 называет биты по-новому (MODE5, BS5, ID0) |
маски по номеру вывода или функции LL | 8 |
на G0 не переключается на PLL, хотя PLLRDY есть |
выход R не разрешён битом PLLREN |
ставить PLLREN вместе с делителями в PLLCFGR |
6 |
| на F0 или G0 UART после первого переполнения замолчал | флаг ORE сам не сбрасывается, как у F1 чтением SR и DR |
сбрасывать записью USART_ICR_ORECF в ICR |
16 |
| после обновления сервера пропали показания приборов | данные писались внутрь контейнера, а не в том | всё, что должно пережить контейнер, — в volumes; down -v — только с копией |
111 |
| в журнале приёмника у всех приборов один адрес | это адрес обратного прокси | прокси ставит X-Real-IP, приёмник берёт адрес из него |
111 |
| прокси отвечает 502 сразу после запуска стенда | приёмник ещё не готов, прокси стартовал раньше | healthcheck и condition: service_healthy |
111 |
| диск сервера заполнился журналами контейнеров | у драйвера json-file нет ограничения размера |
max-size и max-file в logging |
112 |
| упал приёмник телеметрии, а оповещений нет | правила только о приборах: без метрик они молчат | правило up == 0 о самом приёмнике, канал оповещений — отдельный сервис |
112 |
| дежурного будят одиночные выбросы датчика | в правиле оповещения нет for |
условие должно держаться заданное время | 112 |
| русские имена файлов — мусор | нет FF_USE_LFN, кодовая страница или строки UTF-8 при FF_LFN_UNICODE = 0 |
включить, FF_CODE_PAGE, FF_LFN_UNICODE = 2 |
42 |
| медленная запись на флеш через FatFS | сектор 512 → чтение-стирание-запись | буферизовать, крупнее блок | 42 |
статистика: printf перестал работать |
форматирующие функции требуют стек | больше стека или выключить | 33 |
строка до fork напечаталась дважды |
буфер stdout скопирован в потомка |
fflush(NULL) перед fork |
136 |
read вернул −1 без видимой причины |
EINTR: сигнал прервал вызов |
повторить или SA_RESTART |
136 |
read из канала никогда не возвращает 0 |
пишущий конец открыт в самом читателе | закрыть его после fork |
137 |
syslog ничего не пишет |
нет /dev/log в контейнере или минимальной системе |
LOG_PERROR или свой файл |
138 |
read вернул −1 на неблокирующем сокете |
EAGAIN — «пока нечего», не ошибка |
ждать готовности через poll/epoll |
141 |
epoll с EPOLLET пропускает данные |
не дочитали до EAGAIN |
читать до EAGAIN |
141 |
| тысяча вставок в SQLite — секунды | автокоммит на каждую | BEGIN … COMMIT пачкой |
148 |
| «код × 0,806 мВ» на ESP32 врёт на сотни милливольт | опора не 3,3 В, прямая не через ноль | adc_cali_raw_to_voltage с калибровкой |
154 |
ledc_set_duty ничего не меняет |
скважность применяется отдельным вызовом | добавить ledc_update_duty |
153 |
fopen("/fat/лог.txt") вернул NULL |
длинные имена FAT выключены, только 8.3 | включить LFN в menuconfig или имена 8.3 |
155 |
| файлы в SPIFFS занимают втрое больше, чем данные | страница 256 байт и индекс на каждый файл | крупные файлы или FAT | 155 |
printf("%.2f") печатает пустоту |
в newlib-nano (--specs=nano.specs) вывод float выключен ради размера |
ключ -u _printf_float (+13 КБ кода) или печатать целыми частями |
4б |
36.6 Работает иногда
Самый неприятный класс. Общее правило: «иногда» — чаще всего гонка, тайминг или память. Из аппаратных причин — длинные провода, питание и земля: их смотрят осциллографом, а не отладчиком.
| Симптом | Причина | Лечение | § |
|---|---|---|---|
| работает только с отладчиком | гонка: под отладчиком тайминг другой | volatile, секции, ожидание флагов |
3, 16 |
работает при -O0, ломается при -O2 |
нет volatile или гонка |
не «ошибка компилятора» | 3, 30 |
| через час — порча данных или зависание | обычная функция ядра из прерывания | только FromISR |
28 |
| прибор падает через неделю | утечка памяти | следить за худшим остатком кучи | 25 |
| память тает при создании и удалении задач | задача простоя не получает процессор | дать ей время | 26 |
| задачи создаются, потом перестают | кончилась куча | xPortGetMinimumEverFreeHeapSize |
25 |
| свободно много, выделить нельзя | фрагментация | heap_4, реже создавать-удалять |
25 |
| высокоприоритетная задача тормозит без причины | инверсия приоритетов | мьютекс, держать коротко | 30 |
| задача пропускает события | двоичный семафор при частых событиях | считающий или очередь | 28 |
| реакция задачи запаздывает на миллисекунду | забыт portYIELD_FROM_ISR |
добавить | 28 |
| критическая секция не защищает переменную | переменную трогает быстрое прерывание | другая защита или ниже порога | 28, 30 |
| приём по UART теряет байты при работе с дисплеем | длинная критическая секция | мьютекс или привратник | 30 |
| байты с UART теряются под нагрузкой | буфер полон | больше буфер, выше приоритет читателя | 32 |
| данные в буфере портятся | два писателя или два читателя | правило одного | 32 |
| теряются значения уведомления | хранит одно | очередь | 31 |
| дребезг даёт десяток прерываний | механика кнопки | подавление дребезга | 7 |
| UART: первый байт теряется | приёмник включён позже передатчика | порядок включения | 16 |
| UART: приём молчит после потока | не сброшен ORE |
читать SR, затем DR |
16 |
| UART: обрезается конец посылки | ждали TXE вместо TC |
TC перед сном |
16 |
| SPI: данные обрезаются на скорости | CS снят до сброса BSY |
ждать BSY |
18 |
| USB: работает на одном компьютере, не на другом | подтяжка или качество линий | резистор; кабель | 20 |
| USB: пропадает после сна | требует активности | обработка пробуждения | 20 |
| USB: данные теряются при быстрой отправке | хост опрашивает по расписанию | буфер и отправка по готовности | 20 |
| команда по USB CDC не узнаётся | пакет сравнивали с командой, а она пришла кусками | кольцевой буфер и сборка строк в цикле | 20 |
| двоичные данные по CDC обрезаются | strncpy остановилась на нулевом байте |
копировать байты, а не строки | 20 |
| ответ по CDC испорчен | буфер изменили до конца передачи | отдельный буфер, отправка после USBD_OK |
20 |
| SPI флеш работает через раз на макетке | длинные провода на 20 МГц | снизить скорость, короче провода | 41 |
| I²C виснет навсегда | нет таймаутов; отвалившийся провод | таймауты везде | 17 |
стек кончается на printf |
сотни байт под форматирование | запас или отказ от printf |
26 |
| переполнение стека в демоне таймеров | printf или массив в обратном вызове |
не делать этого там | 29 |
| файл нулевой длины после отключения | нет f_close/f_sync |
f_sync после записи |
42 |
| время RTC на F1 изредка прыгает на 18 часов | счётчик прочитан половинами без повтора — младшая перешла через 0xFFFF между чтениями | перечитать старшую половину и повторить | 12 |
| счётчик из потоков не сходится, каждый раз по-разному | гонка: ++ не атомарен, volatile не помогает |
мьютекс или atomic_fetch_add |
139 |
| счётчик в общей памяти считает неверно | нет семафора между процессами | семафор POSIX или XSI | 137 |
| сигнал «потерялся» | обычные сигналы не ставятся в очередь | блокировать маской, а не терять | 136 |
| счётчик из двух задач ESP32 не сходится, хотя есть мьютекс | второе ядро: мьютекс не защищает от него | спин-блокировка portMUX_TYPE или __atomic_* |
152 |
| тактовый вход на длинном проводе ловит лишние фронты | звон несогласованной линии: волна удваивается на конце и отражается | последовательный резистор у вывода до волнового сопротивления линии | 160 |
| I²C работает на столе, сбоит на проводах в корпусе | ёмкость проводов затягивает фронт R·C за предел режима | меньше подтяжка, короче провода, стандартный режим или усилитель шины | 160 |
36.7 Зависает
| Симптом | Причина | Лечение | § |
|---|---|---|---|
| зависает, но не совсем: часть работает | высокоприоритетная задача без ожидания | добавить блокировку | 26 |
| живой, но ничего не делает | взаимная блокировка двух мьютексов | единый порядок захвата | 30 |
зависла на xSemaphoreTake своего мьютекса |
взяла дважды | разобрать вызовы; рекурсивный | 30 |
| все программные таймеры встали разом | обратный вызов заблокировался | ноль ожидания в обратных вызовах | 29 |
| зависает без отладчика | SWO ждёт готовности при выключенном модуле | проверка ITM_TCR_ITMENA |
21 |
| зависает на ожидании готовности RCC | кварц не запустился, таймаута нет | таймаут; проверить кварц | 6 |
| обработчик ждёт на семафоре | ждать в прерывании нельзя | TakeFromISR без ожидания |
28 |
xTimerStart возвращает отказ |
очередь команд полна или демон стоит | см. «все таймеры встали» | 29 |
| синхронизация задач зависает | SetBits + WaitBits вместо Sync |
xEventGroupSync |
31 |
xMessageBufferReceive вечно ноль |
приёмник меньше сообщения, оно застряло | приёмник ≥ максимального кадра | 32 |
Reset буфера — отказ |
кто-то ждёт | остановить читателя | 32 |
| стирание флеш «зависло» | сотни миллисекунд — это норма | ждать BUSY с таймаутом |
41 |
| прибор не спит в режиме без тика | задача просыпается чаще двух тиков | найти её | 33 |
| режим без тика включён, а ток не упал | configPRE_SLEEP_PROCESSING обнуляет аргумент до вызова своей функции |
передавать время как есть или засыпать в функции самим | 33 |
после vTaskSuspend задача мертва |
выводит только vTaskResume |
не путать с ожиданием | 26 |
open FIFO на чтение висит |
нет ни одного писателя | O_NONBLOCK или открыть на запись самому |
137 |
cond_wait — вечный сон |
вызван без захваченного мьютекса или без цикла проверки условия | while (!ready) cond_wait под мьютексом |
139 |
cat /dev/ringdev не завершается |
read драйвера не возвращает 0 при пустом буфере |
вернуть 0 — конец файла | 147 |
Task watchdog got triggered … IDLE1 |
задача на ядре 1 не уступала процессор дольше 5 с | vTaskDelay в цикле или подписка на сторожа |
152 |
Interrupt wdt timeout on CPU0 |
прерывания выключены дольше 300 мс: затянутая критическая секция | сократить секцию | 152 |
36.8 Память и HardFault
| Симптом | Причина | Лечение | § |
|---|---|---|---|
PC в мусоре, UNDEFINSTR |
стек затёр адрес возврата | измерить стек; ловушка | 35, 26 |
PRECISERR, BFAR — маленькое число |
нулевой указатель со смещением | искать NULL |
35 |
IMPRECISERR, PC не туда |
запись ушла в буфер шины | DISDEFWBUF на время отладки |
35 |
UNALIGNED |
uint32_t * на байтовый буфер |
memcpy или упаковка |
35, 3 |
HardFault в обработчике с приоритетом 0 |
обработчик выше системного зовёт ядро | приоритет ≥ порога | 7, 28 |
| буфер DMA «не заполняется» на F4 | буфер в CCM, DMA туда не ходит | обычная SRAM | 9, 41 |
| DMA портит стек | локальный буфер исчез вместе с кадром | статический буфер | 9 |
| данные от DMA не меняются в коде | нет volatile |
добавить | 9, 14 |
порча при heap_5 |
области не по возрастанию адресов | упорядочить | 25 |
| статический буфер потока портит соседа | массив N вместо N + 1 |
+ 1 |
32 |
FATFS объявлена локально |
структура должна жить всё время | статическая | 42 |
| переполнение стека на буфере сектора | 4 КБ локально | статический буфер | 42 |
| буфер под DMA не выровнен | 16/32-разрядный элемент по нечётному адресу | __attribute__((aligned)) |
9, 42 |
| флеш: записали, а прочитали не то | запись без стирания: 1→0 только | стереть сектор | 41 |
| флеш умерла через месяц | 100 000 циклов на сектор; журнал в один сектор | ротация секторов | 41 |
незакреплённые WP#/HOLD# |
ловят наводки | подтянуть к питанию | 41 |
| настройки пропали после выключения посреди сохранения | страницу стёрли, а записать не успели | журнал на двух страницах | 11 |
| после обновления прошивки настройки «съехали» | сменилась раскладка структуры | поле версии, при несовпадении — умолчания | 11 |
| запись настроек обрывается ошибкой на середине | блок больше страницы: соседняя не стёрта | запись не больше страницы или журнал | 11 |
| прерывания запаздывают на десятки мс при сохранении | стирание Flash останавливает выборку команд (F1, один банк) | сохранять в спокойный момент; срочный обработчик в ОЗУ | 11 |
Guru Meditation (Cache disabled but cached memory region accessed) при записи в NVS или обновлении |
обработчик с флагом ESP_INTR_FLAG_IRAM зовёт функцию без IRAM_ATTR или читает константу без DRAM_ATTR, а кэш на время записи выключен; обычные обработчики в это время просто ждут |
всё, что трогает такой обработчик, — в IRAM_ATTR и DRAM_ATTR, либо снять флаг ESP_INTR_FLAG_IRAM |
150 |
malloc на 200 КБ вернул NULL при 300 КБ свободных |
куча ESP32 из банков, самый большой цельный кусок меньше | дробить буфер или MALLOC_CAP_* |
150 |
HardFault сразу после вызова по указателю на функцию |
чётный адрес перехода: CFSR = 0x00020000 (INVSTATE) | брать адрес функции, а не число; разбирать CFSR, HFSR и PC в кадре исключения | 4б |
36.9 Питание и сон
| Симптом | Причина | Лечение | § |
|---|---|---|---|
| после Stop всё медленное | тактирование не восстановлено: HSI вместо PLL | восстановить PLL после пробуждения | 10 |
| ток в сне выше расчётного | выводы в неопределённом состоянии | аналоговый режим или подтяжка | 10 |
| ток сна на десятки мкА выше | подтяжка ~40 кОм на выводе, прижатом снаружи к земле | снять подтяжку или перевернуть логику | 10 |
| ток в Sleep не падает | периферия включена | выключать перед сном | 10, 33 |
| ток в Stop как в Sleep | отладчик оставил биты DBG_STOP в DBGMCU->CR; сброс их не снимает |
снять биты или выключить и включить питание | 10 |
| при пробуждении камень сбрасывается, пока в питании амперметр | предел 200 мкА — шунт 1 кОм, питание падает ниже 2 В | перемычка параллельно прибору | 10 |
| показания мультиметра прыгают в десятки раз | усредняет за доли секунды, всплески попадают через раз | мерить сон и всплеск отдельно, среднее считать | 10 |
| программатор не видит камень после подготовки ко сну | в аналоговый режим ушли PA13 и PA14 | оставлять SWD в маске; прошить с подключением под сбросом | 10 |
| спит, но недолго (≤ 233 мс) | предел SysTick |
ядро на меньшей частоте всё время или свой таймер сна | 33 |
| проснулся — время уплыло | своя функция сна без vTaskStepTick |
скорректировать | 33 |
| слишком частое пробуждение съедает выгоду | выход из Stop/Standby не бесплатен | реже просыпаться | 10 |
| кормление сторожа из прерывания | прерывание живо, программа мертва — сторож молчит | кормить из основного цикла | 12 |
| флаги причины сброса врут | не очищены | RMVF после чтения |
12 |
| причина сброса всегда «кнопка» | PINRSTF стоит почти при любом сбросе |
проверять его последним | 12 |
| WWDG сбрасывает сразу после запуска | счётчик 0x40 — до сброса один тик | запускать с 0x7F | 12 |
| WWDG сбрасывает при кормлении | кормят раньше, чем счётчик опустился до окна W |
кормить внутри окна | 12 |
| будильник RTC будит один раз | не сброшен флаг будильника (ALRAF, у F1 — ALRF): нового фронта на EXTI 17 нет |
сбрасывать флаг в обработчике | 12 |
| обработчик будильника RTC вызывается без конца | не сброшен флаг линии EXTI 17 | EXTI->PR = EXTI_PR_PR17 |
12 |
| будильник F4 не записывается | не сняты ключи WPR или не дождались ALRAWF |
0xCA, 0x53, затем ждать ALRAWF |
12 |
| шум в АЦП/ЦАП | земля, питание, опора | разводка; фильтр; VDDA |
14, 15 |
| электронная нагрузка не опускается до нуля | включён выходной буфер ЦАП | BOFF1: вход ОУ высокоомный |
15 |
| опора АЦП «плавает» | VDDA от USB через диод |
стабилизатор или внешняя опора | 14 |
| показания АЦП плывут вместе с батареей | опора — VDDA, а считали от 3,3 В |
пересчёт через VREFINT |
14 |
| внутренние каналы АЦП дают ноль | не включён TSVREFE |
CR2 у F1, ADC->CCR у F4 |
14 |
переменная RTC_DATA_ATTR обнулилась после esp_restart |
она переживает только глубокий сон, программный сброс загрузчик заливает заново | для сброса — RTC_NOINIT_ATTR и обнуление по ESP_RST_POWERON |
157 |
ESP32 после глубокого сна начинает с app_main |
выход из глубокого сна — это сброс | хранить состояние в памяти RTC, смотреть esp_sleep_get_wakeup_cause() |
157 |
| датчик на ESP32 сажает батарею за недели | на каждом пробуждении долгое подключение к Wi-Fi: 2 с радио дороже суток сна | просыпаться реже, статический IP, канал роутера — в памяти RTC | 157 |
| ESP32 перезагружается, как только включает передатчик | провал шины 3,3 В на пике 240 мА: мало ёмкости у модуля, длинный кабель | 10 мкФ и 100 мкФ и больше у модуля, стабилизатор с малым падением, короткий кабель | 161 |
36.10 Аппаратные
| Симптом | Причина | Лечение | § |
|---|---|---|---|
| DRC: замыкание у корпуса микроконтроллера | дорожка от развязки идёт наискосок через соседние выводы | конденсатор по нормали к ряду выводов, ближним выводом к VDD |
130, 131 |
| кварц не запускается на холодную | конденсаторы не под CL своего кварца |
C1 = C2 = 2 × (CL − Cпар) |
130 |
| дорожка питания греется | ширина не по току, особенно на внутреннем слое | ширина по IPC-2221: 1 А при 10 °C — 0,30 мм снаружи, 0,78 мм внутри | 130 |
| DRC в сборке падает, хотя нарушений нет | --exit-code-violations считает и неразведённые соединения |
ставить ключ, когда плата разведена целиком | 131 |
| плагин DipTrace отработал, а плата не изменилась | файл обмена не перезаписан: DipTrace смотрит только на его изменение, код возврата не читает | переписать тот же plugin_exchange.xml заменой целиком |
132 |
| объект, удалённый плагином DipTrace, остался на плате | узел вычеркнут из XML | ставить Enabled="N" |
132 |
| расстояния в дюймовом файле обмена плывут в третьем знаке | четыре знака в дюймах — шаг 2,54 мкм | считать с допуском в пару микрометров | 132 |
configparser: «File contains no section headers» на проекте Altium |
.PrjPcb начинается с метки порядка байтов UTF-8 |
читать в кодировке utf-8-sig |
133 |
скрипт KiCad не открывает плату Altium, LoadBoard вернул пустое |
формат по файлу не угадывается | указать PCB_IO_MGR.ALTIUM_DESIGNER |
133 |
| после импорта из Altium классы цепей — 0,2 мм вместо 0,1 | в классы попали настройки KiCad, а не правила Altium | задать классы цепей заново | 133 |
| KiCad не открывает свой же IPC-2581 или ODB++ | эти форматы KiCad только пишет | переносить проектом или списком соединений | 134 |
| после импорта списка соединений у деталей нет корпусов | имена посадочных мест в программах разные | сопоставить корпуса по именам, остальные — руками | 134 |
| механик получил плату без деталей | к деталям не привязаны 3D-модели | привязать модели до выгрузки STEP | 134 |
| кварц не той частоты | 8 МГц на плате, 25 в проекте | привести в соответствие | 6 |
| система осталась на HSI | проверен SW, а не SWS |
ждать SWS |
6 |
| пропал HSE — прибор встал | нет CSS | включить, обработать NMI | 6 |
| UART: линии перепутаны | TX на TX |
TX → RX |
16 |
| кириллица в терминале | кодировка терминала | UTF-8 с обеих сторон | 16 |
| I²C: адрес не тот | 0x68 против 0xD0 |
формат 7 бит со сдвигом | 17 |
I²C: ADDR не сбрасывается |
прочитан только SR1 |
SR1, затем SR2 |
17 |
| I²C: с одним модулем работает, с несколькими — нет | подтяжки модулей складываются: пять по 4,7 кОм — 940 Ом, больше 3 мА | оставить подтяжки на одном модуле | 17, 95 |
| I²C: на столе работает, на проводе сбоит | ёмкость провода растягивает фронт: на 400 кГц с 2,2 кОм — 1,71 м | ниже скорость, пересчитать подтяжку, буфер | 17, 95 |
| нет общей земли | датчик на своей плате | соединить | 17, 19, 95 |
| вход сбоит или греется с модулем на 5 В | модуль подтягивает шину к 5 В | снять подтяжки модуля или преобразователь уровней | 62, 95 |
| термистор показывает −40 или +125 и не меняется | обрыв или замыкание делителя: код за краем таблицы | проверять выход за таблицу, не выдавать за температуру | 96 |
| SPI: мусор при нескольких устройствах | два CS активны |
подтяжки на все CS |
18 |
| SPI: дисплей работает, флешка нет | разные режимы на одной шине | переключать режим | 18 |
| SD-карта молчит на CMD0 | неверный CRC7, мало тактов перед командой или скорость выше 400 кГц | CMD0 — 0x40 00 00 00 00 95, 74 такта, делитель 256 | 18 |
| на выходах 74HC595 байты перепутаны | отправлены в прямом порядке | первым — байт дальнего регистра | 18 |
| SWO на F407 выводит мусор | в отладчике частота кварца 8 МГц вместо 168 МГц ядра | частота ядра в настройках, ACPR = 83 для 2 МГц | 21 |
| SWO теряет символы и тормозит программу | по байту линия 2 МГц пропускает 100 000 символов в секунду | писать словами — 160 000, выводить меньше | 21 |
| на клавиатуре нажата лишняя кнопка | три кнопки в углах прямоугольника без диодов: 144 тройки из 560 | диод на каждую кнопку | 22 |
| общий вывод разряда индикатора греется | восемь сегментов по 8 мА — 64 мА через один вывод | ключ разряда — транзистор | 22 |
| CAN без трансивера | прямое подключение | трансивер обязателен | 19 |
| CAN: терминаторы | не два по 120 на концах | ровно два, на концах | 19 |
| USB: определяется как неизвестное | дескрипторы | анализатор протокола | 20 |
| USB: кабель | без экрана, тонкий | другой кабель | 20 |
| SPI флеш: режим | 0 или 3, не 1 и 2 | CPOL=CPHA |
41 |
| ток утекает через висящий вход | вход без подтяжки | подтянуть | 8, 10 |
| ключ реле выгорает, ESP32 сбрасывается при выключении реле | нет диода на катушке: выброс до пробоя ключа | диод параллельно катушке катодом к питанию, у дальних нагрузок — TVS | 161 |
| MOSFET от вывода ESP32 греется, реле щёлкает неуверенно | транзистор не логический: от 3,3 В открыт не полностью | транзистор с низким порогом или NPN с резистором базы | 161 |
36.11 Связь и радио
| Симптом | Причина | Лечение | § |
|---|---|---|---|
модуль Wi-Fi отвечает ERROR на всё |
не та скорость UART или прошлая команда не завершена | скорость; дождаться ответа | 37 |
| разборщик ответов модуля путается | незапрошенное сообщение пришло между командой и ответом | разбирать каждую строку по её началу | 37 |
| брокер отключает раз в полторы минуты | нет PINGREQ чаще keep-alive |
посылать по таймеру | 37 |
CONNACK с кодом 5 |
не авторизован: чужой или пустой токен | свой токен у каждого прибора | 37, 91 |
| соединение с брокером молча рвётся | роутер удалил запись NAT, пока не было трафика | keep-alive | 86 |
| сервер не может достучаться до прибора | прибор за NAT | прибор подключается сам | 86 |
| после переезда сервера все приборы молчат | в прошивке адрес, а не имя | имя в настройках, меняемых по воздуху | 87, 91 |
| сервер не может подняться после сбоя | все приборы повторяют запросы разом | растущая задержка со случайным разбросом | 88 |
| в базе дубликаты измерений | повтор POST после обрыва |
PUT по номеру или ключ запроса |
88 |
| тело запроса обрезано на кириллице | Content-Length в символах |
длина в байтах | 88 |
| балансировщик шлёт всё на один сервер | нет общей зоны у группы серверов | zone в upstream |
88 |
| RS-485 теряет последний байт | передатчик выключен по TXE |
выключать по TC |
89 |
| Modbus: ошибка CRC | байты CRC переставлены | CRC младшим байтом вперёд | 89 |
| сервер CoAP выполнил команду дважды | повтор отправлен с новым номером сообщения | при повторе номер тот же | 89 |
| Zigbee теряет пакеты рядом с Wi-Fi | канал перекрывается с Wi-Fi | каналы 15, 20, 25, 26 | 89 |
| прибор LoRaWAN замолкает на минуты | на SF12 кончилась доля эфира 1 % | реже слать, меньше SF | 89, 93 |
SX1276: RegVersion читается как 0x00 или 0xFF |
провода, режим SPI, NSS |
проверять по порядку | 93 |
| SX1276 не передаёт, хотя регистры записаны | бит LoRa записан не во сне | сон → LoRa → ожидание | 93 |
| SX1276 на чужой частоте | Frf посчитан в 32 битах |
считать в uint64_t |
93 |
| пакеты LoRa не доходят, ошибок нет | разные SF, полоса или слово синхронизации | сверить параметры на обеих сторонах | 93 |
| сетевой сервер молча не видит прибор | адрес или ключ в другом порядке байт | в кадре — младшим байтом вперёд | 94 |
| прибор ABP замолчал после перезагрузки | счётчик кадров начался с нуля | хранить счётчик в энергонезависимой памяти | 94 |
| команды до прибора класса A не доходят | окно RX1 открыто поздно | таймер от события «передача окончена» | 94 |
| молчание прибора не понять | нет контрольной посылки | посылка «я жив» раз в заданное время | 90 |
| карта покрытия врёт | в модели потерь нет стен | стены в модели, план с материалами | 92 |
ESP32: esp_netif запущен, а адреса нет |
не создан цикл событий или не инициализирована NVS | nvs_flash_init() и esp_event_loop_create_default() до драйвера сети |
156 |
| ESP32 не соединяется с Mosquitto 2 | без listener 1883 и allow_anonymous брокер пускает только localhost |
прописать listener и учётные данные клиента | 156 |
| в журнале ESP32 даты 1970 года | SNTP ещё не синхронизировался, часов с батарейкой нет | ждать esp_netif_sntp_sync_wait с тайм-аутом, журналы вести в UTC |
156 |
| ESP32 не виден в сканере BLE после отключения телефона | реклама не перезапускается по событию разрыва | запускать рекламу снова в обработчике BLE_GAP_EVENT_DISCONNECT |
158 |
| по BLE приходят только первые 20 байт | MTU по умолчанию 23, заголовок 3 байта | обмен MTU или передача кусками | 158 |
36.12 Если ничего не подошло
Строки, которых нет в таблице, — это новые грабли, и они тоже чему-то учат. Порядок:
- Свести к минимуму — проект из десяти строк.
- Прочитать регистры — все, что относятся к блоку, в отладчике.
- Сравнить с известным рабочим — пример из пака, чужая плата, другой камень.
- Осциллограф на линию — то, что на проводе, а не то, что должно быть.
- Записать — симптом, причину, лечение. Через год это сэкономит день.
36.13 Итоги
- Пять вопросов до поиска: собирается без предупреждений; доходит
до
main; тактируется; та ли частота; включено ли прерывание везде. - Три приёма: делить пополам, свести к минимуму, читать регистры.
- «Работает только с отладчиком» — гонка. «Через час» — вызов ядра
не оттуда. «Через неделю» — утечка. «При
-O2» —volatile, гонка или неопределённое поведение. - «Код правильный, а не работает» — такт блока, в половине случаев.
- «Всё не то, но пропорционально» — частота: кварц, PLL,
configCPU_CLOCK_HZ. - Зависание с частично живым прибором — задача без ожидания; полностью мёртвый живой прибор — взаимная блокировка.
HardFaultчитается всегда:PCиз кадра, причина изCFSR.- Связь: прибор выходит на связь сам, повторяет с разбросом и помнит счётчики; параметры радио и порядок байт сверяют на обеих сторонах.
Дальше: раздел 36а «Приборы на столе» — мультиметр, осциллограф и логический анализатор: чем смотреть то, чего не видит отладчик.