36. Typical problems: symptom → cause → fix
Every section of the handbook ended with "pitfalls" — here they are gathered together and rearranged. Not by topic, but by what the trouble looks like: you see the symptom, not the cause, and the search has to start from it. More than four hundred rows, and most devices that "don't work for no reason" fall into one of them.
The number in the last column is the section where it is covered in detail.
Terms used in the documentation
Term Meaning troubleshooting finding and fixing faults symptom symptom, the outward sign root cause the underlying cause intermittent occurring only "sometimes" heisenbug a bug that disappears when you try to observe it works on my desk "it works on the bench" sanity check checking the obvious bisect searching by halving
36.1 Order of actions
Before searching the table — five questions in order. A "no" to any of them means it is too early to search further.
1. Does it build without warnings? Not just without errors. A warning about an implicit conversion, an unused value or an uninitialized variable is the answer in half of the cases.
2. Does it reach main? A breakpoint on the first line. If it does not get there —
wrong startup file, vector table, stack (section 11).
3. Is the thing that does not work clocked? Half of all "the code is correct,
but it does not work" cases are a forgotten bit in RCC->AHBENR/APB1ENR/APB2ENR.
Check the register in the debugger, not the code: the code may be written but never reached.
4. Is the frequency right? Read RCC->CFGR and SWS in the debugger instead of trusting
SystemCoreClock. A crystal of the wrong frequency, a PLL that did not start, SWS not checked —
and everything that depends on time is off proportionally.
5. Is the interrupt enabled at every level? Peripheral → NVIC →
global. And the priority is assigned explicitly — by default it is zero, and under
the kernel that is a bug.
Next comes the table.
Three techniques that always work
Halve it. Remove half of the code — does it work now? Then the trouble is in the removed half. Repeat. Ten steps — a thousand lines.
Reduce to the minimum. A separate ten-line project that reproduces the trouble. If it does not reproduce — the trouble is in the interaction, and that is already an answer.
Read registers, not code. The code says what you wanted. The register says what you got. If they differ — the problem is somewhere between them.
36.2 Does not build or link
| Symptom | Cause | Fix | § |
|---|---|---|---|
a colleague gets file not found on a header of the same project |
an absolute path in Include Paths | paths relative to the project file: .\App;.\Board;.\Drivers |
4а |
undefined symbol at link time, although the file is in the folder |
the file is not added to a µVision project group | add the file to the group | 4а |
Error #540: … component is not available … pack … is not selected |
pack versions are not pinned, and the newest pack is organized differently | pin the versions in Select Software Packs | 4а |
| "uses RTOS support that is part of a Legacy Pack" | the old RTX OS is enabled in the target of someone else's project | in the target options choose Operating system: None | 4а |
SVC_Handler defined twice |
handler stubs remained in the interrupt file | delete the three stubs; keep the name substitution in the configuration | 24 |
| unresolved reference to a kernel function that exists in the sources | its INCLUDE_ is not enabled |
add the macro to FreeRTOSConfig.h |
24, 33 |
unresolved reference to vApplicationGetIdleTaskMemory |
static allocation is enabled | write both memory-supplying functions | 24 |
#error from FreeRTOS.h |
a mandatory macro is not defined | the message names it | 33 |
error on pvPortMalloc |
no heap file is included | add one heap_N.c |
25 |
error on xPortGetMinimumEverFreeHeapSize |
heap_1/2/3 is included |
switch to heap_4 |
25 |
__forceinline does not compile |
the RVDS port used with Arm Compiler 6 |
take portable/GCC/ARM_CM3 |
24 |
| software timers do not build | no INCLUDE_xTaskGetSchedulerState or configTIMER_* |
add them | 33 |
| the firmware suddenly got heavier | timers, groups, buffers are enabled — but only one is needed | disable the extras | 24 |
'else' without a previous 'if' after a macro call |
a multi-statement macro without do { } while (0) |
wrap the body in do { … } while (0) |
127 |
| after moving to C23 — "too many arguments to function call" | a declaration f() without void: in C23 this is a function with no parameters |
write a prototype with parameter types | 128 |
| "unknown type name 'constexpr'" | the standard is below C23 or the compiler has no C23 | -std=c23 on armclang 6.24 or #define / enum |
128 |
__stack_chk_fail not found at link time |
-fstack-protector-strong is enabled, but there is no handler and no guard word |
write __stack_chk_guard and __stack_chk_fail |
129 |
code from F4 does not build on F0 or G0: __set_BASEPRI unknown |
Cortex-M0 and M0+ have no BASEPRI |
critical section through PRIMASK (__disable_irq) |
7 |
| on F0, after your own bootloader, interrupts go to its table | Cortex-M0 has no VTOR |
copy the table to the start of RAM and set MEM_MODE in SYSCFG->CFGR1 |
7 |
undefined reference, although -l is given |
the library comes on the command line before the object that uses it | -l after the sources and objects |
143 |
relocation R_X86_64_32 … recompile with -fPIC |
an object for a .so was built without -fPIC |
rebuild with -fPIC |
143 |
libpq-fe.h: No such file |
the libpq headers live in /usr/include/postgresql |
-I$(pg_config --includedir) |
148 |
insmod: Invalid module format |
the module's vermagic does not match the kernel |
build against your kernel's headers | 147 |
class_create does not build |
since kernel 6.4 it takes one argument, before that two | branch on LINUX_VERSION_CODE |
147 |
fatal error: esp_flash.h: No such file in an ESP-IDF project |
the component has REQUIRES — dependencies are now explicit only |
add the component to REQUIRES |
151 |
ledc_timer_config returned ESP_FAIL |
frequency × 2^resolution exceeds 80 MHz | lower the resolution or the frequency | 153 |
| the firmware built, but the image is 0 bytes and the board is silent | your own linker script has no KEEP(*(.isr_vector)) and ENTRY, and --gc-sections threw everything out |
restore ENTRY(Reset_Handler) and KEEP, check --print-memory-usage after the build |
4в |
36.3 Does not start
| Symptom | Cause | Fix | § |
|---|---|---|---|
does not reach main |
startup file, vector table, stack pointer | a breakpoint in Reset_Handler |
11 |
HardFault right after vTaskStartScheduler |
the names of the three handlers are not substituted | the vPortSVCHandler macros and the others |
24 |
| builds, but tasks do not switch | the same: the vectors lead to empty stubs | the same | 24 |
halt inside port.c at start-up |
configASSERT: interrupt priority or grouping |
call stack; PRIGROUP ≤ 3 |
24, 28 |
stuck in vPortValidateInterruptPriority |
an interrupt above the threshold called a kernel function | priority ≥ 5 or remove the call | 28 |
| halt right after a task starts | the task returned from its function | an infinite loop or vTaskDelete(NULL) |
26 |
| out of memory from the very first run | a 10 KB heap on a chip with 20 | reduce it or use static allocation | 25 |
| the clock "resets" on every reboot | the RTC is initialized unconditionally | check the initialization flag in the backup domain | 12 |
| instant exit from Standby | CWUF not cleared |
clear it before entering | 10 |
cannot open shared object file at start-up |
the library is not in RUNPATH, LD_LIBRARY_PATH or the cache; the current directory is not searched |
rpath $ORIGIN or install it into the system |
143 |
insmod: Operation not permitted |
no privileges, or a container | root on a real machine |
147 |
| ESP32 does not start with peripherals connected | a sensor pulls a strapping pin at reset; GPIO12 pulled up switches the flash to 1.8 V | free pins 0, 2, 5, 12, 15 during reset | 153 |
| ESP32 reboots when Wi-Fi is turned on | a dip below the 2.43 V threshold: a weak regulator or cable | a supply with headroom for radio peaks | 150 |
| the module has 4 MB, but the program sees 2 | the flash size in menuconfig defaults to 2 MB |
CONFIG_ESPTOOLPY_FLASHSIZE_4MB=y |
150 |
| ESP32 is back on the old version after an over-the-air update | the new firmware restarted before confirmation — the bootloader rolled it back | confirm with esp_ota_mark_app_valid_cancel_rollback() after a self-test |
159 |
| after flashing with a programmer, ESP32 boots the old application | otadata still holds an entry for the ota_0 partition |
erase otadata or the whole flash memory |
159 |
36.4 The peripheral is silent
| Symptom | Cause | Fix | § |
|---|---|---|---|
ADC code from F1 does not build on F0 or G0: no SQR3, CR2 |
the F0/G0 ADC has no sequence — channels are marked with bits | a mask in CHSELR, start with ADSTART, end with EOC |
14 |
G0: the ADC never gives ADRDY, calibration never finishes |
the ADVREGEN regulator is not enabled or the 20 µs after it were not waited out |
ADVREGEN, a 20 µs pause, then ADCAL and ADEN |
14 |
| G0: after a channel change the first measurement is from the old channel | the new CHSELR mask does not take effect immediately |
after writing CHSELR wait for CCRDY |
14 |
G0: supply voltage via VREFINT reads 10 % high |
the factory code was taken at 3.0 V, while the formula uses 3300 | VREFINT_CAL_VREF from the LL of your series |
14 |
I²C code from F1 does not build on F0 or G0: no SR1, DR, CCR |
F0/G0 have a different I²C block | TIMINGR, the exchange through CR2 (NBYTES, AUTOEND), flags in ISR |
17 |
F0/G0: the program hangs waiting for TXIS |
nobody answered the address — NACKF arrived |
wait for TXIS and NACKF together, clear NACKF in ICR |
17 |
| F0/G0: after the first exchange the block is "busy" | STOPF not cleared after AUTOEND |
write STOPCF to ICR |
17 |
SPI code from F1 does not build on F0 or G0: no DFF |
in the new SPI the data size is the DS field in CR2 |
DS = size − 1, 0111 for 8 bits |
18 |
| F0/G0: SPI sends an extra zero byte, reception is shifted by a byte | a 16-bit write to DR with 8-bit frames |
write and read DR as a byte: *(volatile uint8_t *)&SPI1->DR |
18 |
F0/G0: RXNE does not rise after one byte |
the receive FIFO threshold is two bytes | the FRXTH bit in CR2 |
18 |
| G0: the DMA channel is configured and enabled, but nothing is transferred | the request is not routed to the channel — DMAMUX not written | DMAMUX1_ChannelN->CCR = request number from LL; DMA channel N+1 ↔ DMAMUX channel N |
9 |
DMA code from F1 does not build on F0 or G0: DMA_CCR4_DIR, DMA1EN |
the CCR bits have no channel number, on F0 the clock bit is DMAEN |
DMA_CCR_DIR, RCC_AHBENR_DMAEN |
9 |
| F0: DMA from USART1 goes to the wrong channel | the mapping differs from F1: USART1_TX is channel 2, with USART1TX_DMA_RMP — 4 |
the F0 mapping table or the remap bit in SYSCFG->CFGR1 |
9 |
sleep code from F1/F0 does not build on G0: no PWR->CR, PDDS |
on G0 the mode is the LPMS field in CR1, the flags are in SCR |
LL_PWR_MODE_* in LPMS, CWUF1/CSBF in SCR, EWUP1 in CR3 |
10 |
G0: a Flash write gives SIZERR or PGAERR |
SIZERR — a byte or halfword write; PGAERR — two words not from the same double word |
double words at an address that is a multiple of 8: two 32-bit words in a row | 11 |
Flash code from F1/F0 does not build on G0: no FLASH->AR |
on G0 a page is erased by its number | the PNB field in CR |
11 |
timer code from F1/F0 does not build on G0: no RCC->APB1ENR |
on G0 the clock registers are APBENR1/APBENR2 |
RCC_APBENR1_TIM3EN, RCC_APBENR2_TIM1EN |
13 |
| F0/G0: the PWM frequency is lower than calculated — by 1.5 times (F0) or by 1.125 times (G0); by 9 or 4.5 times if the system stayed on HSI | PSC calculated for the 72 MHz of F1 |
timer clock 48 (F0) or 64 MHz (G0): PSC 47 or 63 for a 1 MHz count; check that the system moved to the PLL |
13 |
| the code is correct, the register does not change | the block's clock is not enabled | the bit in RCC->xxxENR; check it in the debugger |
6 |
HardFault with PRECISERR, BFAR = a peripheral address |
the same: access to an unclocked block | the same | 6, 35 |
a write to PWR->CR has no effect |
PWR on APB1 without a clock |
RCC_APB1ENR_PWREN |
10 |
| the timer counts, but nothing on the pin | the pin is not in alternate function mode | configure the GPIO |
13 |
| timer settings do not take effect | no update event | the UG bit or wait for an overflow |
13 |
TIM1 is silent |
MOE in BDTR is not set |
set it | 13 |
| the timer interrupt does not arrive | HAL_TIM_Base_Start only starts the counter, UIE is not set |
HAL_TIM_Base_Start_IT |
13 |
| the BluePill LED does not respond to PWM | PC13 is not connected to a timer |
an external LED on a channel pin, for example PA6 |
13 |
| a table does not go to PWM through DMA | the pacing timer does not have the UDE request enabled |
DIER = TIM_DIER_UDE |
9 |
| DMA takes every second value of the table | MSIZE is 32 bits with a uint16_t table |
sizes by element and by register | 9 |
| the interrupt does not arrive | the chain: port clock → EXTI → NVIC → global | check along the chain | 7 |
| the handler is called endlessly | the EXTI->PR flag is not cleared |
write a one | 7 |
| I²C does not work at all | no pull-ups | 4.7 kΩ on both lines | 17 |
| I²C: conflict with the slave | push-pull pins | open-drain | 17 |
| SPI does not respond | CPOL/CPHA mode, MOSI/MISO, CS, speed |
check in order | 18 |
| SPI drops out of master mode | SSI not set |
set it together with SSM |
18 |
| CAN does not receive although transmission works | no filter is configured | at least one that accepts everything | 19 |
| a lone CAN node repeats a frame endlessly | nobody acknowledges: the node is passive but not in bus-off | a second node or loopback | 19 |
| USB is not detected | D+ pull-up, 48 MHz, HSI instead of a crystal | 1.5 kΩ resistor; crystal; frequency | 20 |
| CAN and USB together on F103 | shared buffer memory | one or the other | 19, 20 |
| SWO is silent | SWD, not JTAG; TRACE_IOEN; TRCENA; the key; the channel; debugger settings |
in order | 21 |
| SWO does not work on a clone debugger | the clone does not accept trace | a different debugger | 21 |
PB3 does not work as GPIO |
it is taken by SWO | one or the other | 21 |
| DMA does not start | the enable on the peripheral side is forgotten (DMAT, DMA in ADC_CR2) |
set it | 9 |
| DMA on F4 does not start a second time | flags from the previous transfer remain | clear them in IFCR |
9 |
| there is no DAC in the chip | it simply is not there | read the datasheet before layout | 15 |
| the watchdog fires while debugging | DBGMCU->CR is not set |
stop the watchdog in debug | 12 |
| SPI flash is written "in vain" | Write Enable forgotten |
before every write and erase | 41 |
f_mount → FR_NO_FILESYSTEM |
the medium is not formatted | f_mkfs |
42 |
gpio_set_direction returned ESP_ERR_INVALID_ARG |
pins 34–39 are input only, numbers 24 and 28–31 do not exist | take another pin | 153 |
ADC2 answers ESP_ERR_TIMEOUT |
with Wi-Fi on, the second ADC block is taken by the radio | move the input to ADC1 (GPIO32–39) | 154 |
adc_continuous_config → ESP_ERR_INVALID_ARG |
the rate is below 20 kHz or ADC2 is selected | 20 kHz … 2 MHz and ADC1 only | 154 |
36.5 Works, but not right
| Symptom | Cause | Fix | § |
|---|---|---|---|
| all delays are wrong, but proportionally | configCPU_CLOCK_HZ ≠ the real frequency |
make it match RCC | 33 |
| the timer frequency is twice as high | the clock doubling on APB1 when the prescaler ≠ 1 | account for the ×2 | 13 |
| blinks twice as slow as calculated | toggling every 0.5 s gives a period of 1 s | blink period = two toggles | 13 |
| brightness jumps to maximum when the encoder is turned left | CNT was taken as a position and wrapped through zero |
a signed difference of readings, an accumulator with limits | 13 |
| the DAC/signal frequency is wrong | the same doubling | the same | 15 |
| delays got shorter after changing the tick | delays are written as plain numbers | pdMS_TO_TICKS everywhere |
24 |
| the task period is longer than specified | vTaskDelay counts from the call |
xTaskDelayUntil |
26 |
| two tasks with different priorities behave as equals | a priority above the maximum is silently clamped | ≤ configMAX_PRIORITIES − 1 |
26 |
| the stack headroom looks four times smaller | the function returns words | ×4 | 26 |
vTaskList shows a waiting task as suspended |
the report takes an indefinite wait for a notification for a suspension | this is normal; eTaskGetState returns eBlocked |
26 |
| the counter counts less | a race on the increment | a critical section | 30 |
mutex: Give returns a failure |
the mutex was not taken; for a recursive one — it is given by a non-owner | every Give has its own Take in the same task |
30 |
| a stopped timer started by itself | xTimerChangePeriod starts it |
xTimerStop right after |
29 |
| a mask-driven output does not turn off when the condition disappears | the code waited for group bits, but the group does not report a bit being cleared | wait for a change indication, evaluate the rules yourself | 31 |
| outputs are computed from "stale" inputs | the group was re-read after waking up | parse the value that xEventGroupWaitBits returned |
31 |
| task shares in the run-time statistics jump around | the counter ticks less often than tasks switch | DWT->CYCCNT or a fast timer |
26 |
| the chip stops when a task is created | configASSERT on a priority not less than configMAX_PRIORITIES |
a priority below the maximum | 26 |
| a device with a hung task does not reboot | the watchdog is fed from a software timer without checks: the daemon is always alive | feed only when all tasks have checked in | 29, 12 |
| the button-polling task "sticks" | xQueueSend waits for space in a full queue |
wait 0 and count the losses | 27 |
| one press gives several events | contact bounce | three identical reads in a row | 27, 8 |
| the timer fires late | the daemon priority is below a busy task | daemon at maximum | 29 |
| cannot get finer than a millisecond | the resolution is the tick | a hardware timer | 29 |
| a task wakes up in a loop without an event | the group flag is not cleared on exit | clearOnExit = pdTRUE |
31 |
| a flag set from an interrupt arrives with a delay | it goes through the timer daemon | enable timers, raise the daemon | 31 |
| event group bit 24 does not work | the top byte is used by the kernel | bits 0…23 | 31 |
| the task wakes up on every byte | trigger level 0 or 1 | a sensible threshold | 32 |
| the ADC is off after overclocking | ADCCLK exceeded |
prescaler | 14 |
| ADC readings are low and drift | sampling time too short | increase SMPR |
14 |
| ADC: only the first channel is read | scanning without DMA | enable DMA | 14 |
| ADC: error on F1 | no calibration | CAL after power-up |
14 |
| the DAC does not reach the rails | the output buffer is enabled | this is normal; or disable it | 15 |
| the DAC sags under load | the buffer is disabled, the load is low-impedance | enable the buffer | 15 |
| the DAC signal is stepped | too few points or no filter | table + RC | 15 |
| UART: garbage instead of text | baud rate or divider | recalculate BRR |
16 |
| SWO: garbage instead of text | the core frequency in the debugger ≠ the real one | correct it in the settings | 21 |
| SWO loses characters | the speed is too low for the volume | raise the speed, print less | 21 |
| SPI: shifted by a bit | wrong phase | CPHA |
18 |
| CAN: wrong bit rate | timing fields not reduced by one | −1 in each field |
19 |
| CRC continues the previous calculation | not reset | CRC_CR_RESET |
12 |
| the watchdog timeout is cut too fine | LSI drifts by tens of percent | double margin | 12 |
| DS18B20 returns exactly +85 °C | read before the conversion finished, or the sensor restarted from a power dip | wait the full conversion time; 85.000 — measure again | 96 |
| frost turns into thousands of degrees | the DS18B20 code was read as unsigned | cast to int16_t |
96 |
| DHT22 is silent | the start pulse is short or it is polled more often than once every 2 s | 2 ms (DHT11 — 18 ms), not more often than once every 2 s | 97 |
| DHT22 shows thousands of degrees in frost | sign-and-magnitude was read as two's complement | the top bit is the sign | 97 |
| the SHT31 CRC never matches | the 1-Wire CRC function was used | polynomial 0x31, initial value 0xFF, most significant bit first | 97 |
| pressure is meaningless with a correct circuit | the BMP280 calibration was not read or was parsed with the wrong byte order | 24 bytes from 0x88, least significant first | 98 |
| BME280 gives no humidity | the module has a BMP280: register 0xD0 holds 0x58 | check the chip ID before use | 98 |
| barometric altitude drifts by hundreds of meters | sea-level pressure changes with the weather: 30 hPa is 245.1 m | p0 from a weather service or measure relative to the start | 98 |
| MPU-6050 returns only zeros | it sleeps after power-up | write 0 to PWR_MGMT_1 (0x6B) |
99 |
| ADXL345 readings are garbage with a correct circuit | parsed most significant byte first, as on the MPU-6050 | ADXL345 sends the least significant byte first | 99 |
| the gyroscope angle drifts by tens of degrees per minute | the zero offset is being integrated | calibrate the zero at rest, correct with the accelerometer | 99 |
| HC-SR04 distance reads a few percent high in frost | temperature is not accounted for: a meter at 0 °C reads as 1037 mm | include temperature in the echo conversion | 100 |
| the HC-SR04 timeout fires on every measurement | without URS, resetting the counter on the echo edge raises the update flag |
TIM_CR1_URS |
100 |
| HC-SR501 does not see a sitting person | the sensor detects motion, not presence | account for the hold time or use a different sensor | 100 |
| INA219 shows zero current and power | the calibration register was not written: it is empty after power-up | write CAL at start-up | 101 |
| ACS712 shows current with no load | zero was taken as half scale, but the sensor supply is not exactly 5 V: at 4.9 V — −268 mA | measure zero with no load | 101 |
| the INA219 reading "froze" at high current | more than ±320 mV across the shunt — the shunt register hit ±32000; the OVF bit may not be set | check the shunt register; a smaller shunt or a different gain | 101 |
| empty scales show 39 kg | the 24-bit HX711 code without sign extension: −1 became 16777215 | sign-extend | 102 |
| after changing the HX711 gain the first sample is "wrong" | the gain applies from the next sample | discard the first sample | 102 |
| the scales occasionally show wild values | an interrupt stretched a high level on SCK beyond 60 µs — the HX711 went to sleep | disable interrupts for the pulse burst | 102 |
| the photoresistor shows 0 or 4095 | not light, but an open or short in the divider | check the divider; the extreme codes mean a fault | 103 |
| BH1750 in the sun froze at 54612 lx | the code hit 65535 at MTreg 69 | MTreg 31 | 103 |
| the MQ sensing layer resistance is a third higher | the ADC divider sits in parallel with RL: 7.5 kΩ instead of 10 | calculate with RL loaded by the divider | 103 |
| −250 °C from MAX31855 reads as 3846 °C | a 14-bit code without sign extension | sign-extend | 104 |
| MAX6675 readings do not change | CS is pulled low more often than once every 0.22 s: the read interrupts the conversion | read once every 250 ms | 104 |
| a thermocouple in a furnace shows 1184 instead of 1200 °C | the linear MAX31855 conversion at 41.276 µV/°C | correction from the type K table | 104 |
| Ethernet frames go out, but nobody answers | the type was written least significant byte first: 0x0608 instead of 0x0806 | numbers in frame fields go most significant byte first | 105 |
| the device sees its neighbors, but not the internet | no gateway, or the mask is wider than the real one: a foreign address is looked up via ARP | mask and gateway as in the network | 105 |
| ARP answers, but ping is silent | a wrong IP or ICMP checksum — the receiver silently drops the packet | compute the checksum with zero in its field | 105 |
| a TCP connection is "alive" 15 minutes after the cable was cut | retransmissions with doubling: 924.6 s until failure | your own link check | 106 |
| a device behind NAT stopped receiving commands | the NAT entry expired: for UDP — after 30 s of silence | keep-alive more often than the timeout | 106 |
| TCP tops out at hundreds of kilobits on a fast link | a small window: 2048 bytes at 50 ms — 0.33 Mbit/s | a bigger window or a shorter round-trip time | 106 |
| the device does not get an address via DHCP | the "broadcast reply" flag is not set: the reply went to an address the device does not have yet | flag 0x8000 in DHCPDISCOVER | 107 |
| the device goes to the server's old address | the DNS answer is cached longer than the record's lifetime | cache no longer than the TTL | 107 |
| TLS fails certificate verification | the device clock is set to the year 2000: the certificate is "not yet valid" | synchronize the clock before TLS | 107 |
| the reply from another network does not arrive, although the router passed the request | the recipient has no return route: the reply went to its default gateway | a route back through the same router | 108 |
| small packets go through the VPN, but large ones are lost | the tunnel MTU is 1420: a 1500-byte packet does not fit | MTU 1420, MSS 1380 | 108 |
| a neighbor's Wi-Fi on channel 3 interferes more than on channel 1 | adjacent channels overlap: 5 MHz spacing with a 20 MHz width | channels 1, 6 or 11 | 108 |
| Ada firmware: "cannot find entry symbol _start", image at 0x8000 | the generic light runtime has no memory map of the chip | your own crt0.S and link.ld or a runtime for the board | 113 |
| the Ada build fails on warnings about Last_Chance_Handler | -gnatwe turns check reminders into errors | keep the checks, remove -gnatw.X | 113 |
| gnatprove "proved" everything without a precondition | it took old results from the same directory | run in a clean copy | 113 |
| the Rust vector table is a kilobyte out of nowhere | no chip crate: cortex-m-rt reserves all 256 words | a chip crate with its interrupts | 114 |
| Cargo built the program, but not for the chip | --target was forgotten | --target thumbv7m-none-eabi or thumbv7em-none-eabihf | 114 |
| the crate built differently on another machine | Cargo.lock is not committed | Cargo.lock into the repository | 114 |
| PlatformIO: built yesterday, not today | the platform is not pinned and got updated | ststm32@20.0.0 in platformio.ini | 115 |
| CMake fails the compiler check for a bare-metal chip | the trial build tries to link a program | CMAKE_TRY_COMPILE_TARGET_TYPE = STATIC_LIBRARY | 115 |
| the PlatformIO compiler turned out to be old | the default toolchain of ststm32 20.0.0 is GCC 7.2.1 | your own package in platform_packages | 115 |
| the model on the board answers differently than on the computer | features are not scaled as in training | mean and spread go into the firmware together with the weights | 116 |
| k-NN does not fit in RAM | it stores the whole training set: 600 examples — 4800 bytes of features | a threshold, a tree or a small network | 116 |
| 100 % on the training set, below the threshold on the validation set | the model memorized the examples | accuracy only on data the model has not seen | 116 |
| a peak in the vibration spectrum that the mechanism does not have | aliasing: 160 Hz sampled at 256 Hz shows up as 96 Hz | a sampling rate with margin and a filter before the ADC | 117 |
| the firmware grew by 77 KB from a single FFT | arm_rfft_fast_init_f32 pulls in tables of all sizes | arm_rfft_fast_init_256_f32 — for your own size | 117 |
| the bearing is recognized one time and not the next | the "main peak frequency" feature: a strong rotation tone masks the defect | energy in the defect frequency band | 117 |
| the model is 83 % correct in Python, much worse on site | windows of the same recording ended up in both training and validation | split by recordings: the honest estimate is 75.9 % | 118 |
| it finds almost no defects despite high overall accuracy | a rare class: 21 windows out of 861, the model gains by ignoring it | class weights: 180 out of 180 instead of 60 | 118 |
| the model considers a healthy motor unbalanced | the sensor mount has its own sensitivity | calibration at installation: 410 → 481 out of 540 | 118 |
| int8 outputs on the board differ by one from TensorFlow Lite | CMSIS-NN by default rounds twice when requantizing: only 533 out of 540 windows matched | CMSIS_NN_USE_SINGLE_ROUNDING — all 540 bit for bit | 119 |
| after quantization the model answers with a single class | the input was quantized not with the input tensor's scale and zero point, or without normalization | scale and zero point from the .tflite, normalization as in training | 119 |
| a strong signal cannot be told from a very strong one | saturation: the representative dataset never saw such amplitudes | all classes and modes in the representative dataset | 119 |
| the normal-behavior model does not see increased vibration | the shared baseline includes all motors: imbalance at a 1 % threshold found in 45 windows out of 400 | the device's own baseline after installation — 400 out of 400 | 120 |
| more false alarms than the threshold promised | the threshold was chosen on other motors: 49 out of 1200 instead of 1 % | its own baseline or a threshold from the first hours of operation | 120 |
| the device got used to a growing defect | the baseline adapts to new windows | retrain the baseline only on command, keep the score history | 120 |
| the state-of-charge estimate drifts on an old battery | a filter with the datasheet R0 attributes the extra voltage drop to charge: 5.6 % | R0 in the filter state: 0.7 % | 121 |
| added a state to the filter — it got worse | the current offset is poorly observable: estimated 0.085 A instead of 0.05 | do not add states the measurement cannot see | 121 |
| charge from voltage under load is off by tens of percent | the voltage was measured with the I·R0 drop and the RC network | the table only after rest, under load — the filter | 121 |
| the released firmware version has -dirty | built from uncommitted changes, the build cannot be repeated | release only from a clean tree, version from git describe | 109 |
| firmware 1.9.3 is considered newer than 1.10.0 | versions are compared as text | compare as numbers: major, minor, patch, commits after the tag | 109 |
| unknown which commit broke the build | going through twenty commits by hand | git bisect run: commit 13 out of 21 in 5 checks | 109 |
| the firmware image differs every time | it contains the build time, directory paths and the build ID | SOURCE_DATE_EPOCH, -ffile-prefix-map, --build-id=none — the same image byte for byte | 110 |
| a test failed, but the release went out anyway | the pipeline does not check the exit code | the stage fails on any failed check, no artifact is created | 110 |
| the build suddenly changed without code changes | the "latest version" compiler updated itself | the compiler version is pinned in the build image | 110 |
| a timeout fires early after a long run | comparison with start + timeout, which overflowed: after 8 ticks instead of 100 | (uint32_t)(now - start) >= timeout | 122 |
| the check ~x == 0xF0 for uint8_t is always false | promotion to int: ~0x0F is -16 | (uint8_t)~x == 0xF0 | 122 |
| the comparison is right on the computer, wrong on the board | a different data model: (long)-1 < 1u gives 1 on x86-64 and 0 on Cortex-M | types from stdint.h, explicit casts, _Static_assert | 122 |
| works with -O0, breaks with -O2 | undefined behavior: signed overflow, strict aliasing | a build with UB traps and -Wall; bits through memcpy | 123 |
| an overflow check disappeared in the release build | the check comes after the operation: the compiler replaced x + 1 > x with 1 | check before the operation or use __builtin_add_overflow | 123 |
| HardFault on a UDF instruction in debug firmware | an undefined-behavior trap fired | the address in the exception frame is the faulty line | 123 |
| a loop waiting for a flag from an interrupt does not wait or hangs | the flag is not volatile: armclang turned the wait into BX lr | volatile on the flag | 124 |
| an event counter from an interrupt sometimes lags | events++ is LDR, ADDS, STR, with an interrupt between them | atomic_fetch_add (LDREX/STREX) or a critical section | 124 |
| tables ate up the RAM | without const a table is copied from flash memory into .data | const — the table stays in flash memory | 124 |
| HardFault when calling a handler from a table | a null pointer or a table in RAM corrupted by an out-of-bounds write | index and pointer checks, a const table in flash memory | 125 |
| a command is not found in the table, although it is there | the table for bsearch is not sorted by name | a table-order test on the computer | 125 |
| HardFault on Cortex-M0 while parsing a packet | (uint32_t )(buf + 1) is an unaligned access | memcpy or byte-wise parsing: the compiler chooses LDRB itself | 126 |
| the receiver sees garbage in the packet | a struct with padding bytes was sent | serialize the fields byte by byte | 126 |
| numbers from the sensor are byte-swapped | the sensor sends the most significant byte first, Cortex-M stores the least significant first | byte-wise parsing with an explicit order, or REV | 126 |
| the error message belongs to the neighboring code | the enum and the message table are written separately | an X-macro from a single list or _Static_assert on the table length |
127 |
| garbage in the terminal after changing the bus frequency | the UART divider was not recalculated, the baud error exceeds 2 % | check the divider in #if with #error at build time |
127 |
| a task corrupts someone else's memory on a long packet | a variable-length array on the stack: the size is taken from the packet | -Wvla, an array of the maximum size with a length check |
128 |
| a crash after a long packet, a return from a function into nowhere | memcpy into a local buffer without a length check overwrote the stack frame |
a length check before copying, -fstack-protector-strong |
129 |
| the state machine froze after a fault | a corrupted state variable, the switch has no default |
default — go to a safe state |
129 |
| on G0 a falling-edge interrupt loops forever | the flag is cleared in PR as on F4, but on G0 a falling edge is flagged in FPR1, a rising edge in RPR1 |
clear it by writing a one to FPR1 or RPR1 |
7 |
on G0 the button is always "pressed", IDR reads zero |
after reset G0 pins are analog (MODER = 11), the digital input is off |
switch the pin to input: MODER = 00 |
8 |
code for F0 does not build on G0: GPIO_MODER_MODER5 unknown |
the G0 header names the bits differently (MODE5, BS5, ID0) |
masks by pin number or LL functions | 8 |
on G0 it does not switch to the PLL, although PLLRDY is set |
output R is not enabled with the PLLREN bit |
set PLLREN together with the dividers in PLLCFGR |
6 |
| on F0 or G0 the UART went silent after the first overrun | the ORE flag is not cleared by itself, as on F1 by reading SR and DR |
clear it by writing USART_ICR_ORECF to ICR |
16 |
| device readings disappeared after a server update | data was written inside the container, not into a volume | everything that must outlive the container goes into volumes; down -v only with a backup |
111 |
| in the receiver log all devices have the same address | it is the reverse proxy's address | the proxy sets X-Real-IP, the receiver takes the address from it |
111 |
| the proxy answers 502 right after the stack starts | the receiver is not ready yet, the proxy started earlier | healthcheck and condition: service_healthy |
111 |
| the server disk filled up with container logs | the json-file driver has no size limit |
max-size and max-file in logging |
112 |
| the telemetry receiver went down, but there are no alerts | the rules are only about devices: without metrics they stay silent | an up == 0 rule for the receiver itself, the alert channel as a separate service |
112 |
| the on-call engineer is woken up by single sensor spikes | the alert rule has no for |
the condition must hold for a set time | 112 |
| Cyrillic file names are garbage | no FF_USE_LFN, the code page, or UTF-8 strings with FF_LFN_UNICODE = 0 |
enable it, FF_CODE_PAGE, FF_LFN_UNICODE = 2 |
42 |
| slow writing to flash through FatFS | a 512 sector → read-erase-write | buffer, use larger blocks | 42 |
statistics: printf stopped working |
formatting functions need stack | more stack or disable it | 33 |
a line printed before fork appeared twice |
the stdout buffer was copied into the child |
fflush(NULL) before fork |
136 |
read returned −1 for no visible reason |
EINTR: a signal interrupted the call |
retry or SA_RESTART |
136 |
read from a pipe never returns 0 |
the write end is open in the reader itself | close it after fork |
137 |
syslog writes nothing |
no /dev/log in a container or a minimal system |
LOG_PERROR or your own file |
138 |
read returned −1 on a non-blocking socket |
EAGAIN means "nothing yet", not an error |
wait for readiness via poll/epoll |
141 |
epoll with EPOLLET misses data |
the socket was not read until EAGAIN |
read until EAGAIN |
141 |
| a thousand inserts into SQLite take seconds | autocommit on every one | BEGIN … COMMIT in a batch |
148 |
| "code × 0.806 mV" on ESP32 is off by hundreds of millivolts | the reference is not 3.3 V, the line does not pass through zero | adc_cali_raw_to_voltage with calibration |
154 |
ledc_set_duty changes nothing |
the duty cycle is applied by a separate call | add ledc_update_duty |
153 |
fopen("/fat/лог.txt") returned NULL |
FAT long names are disabled, only 8.3 | enable LFN in menuconfig or use 8.3 names |
155 |
| files in SPIFFS take three times more than the data | a 256-byte page and an index for every file | large files or FAT | 155 |
printf("%.2f") prints nothing |
in newlib-nano (--specs=nano.specs) float output is disabled to save size |
the -u _printf_float option (+13 KB of code) or print integer parts |
4б |
36.6 Works sometimes
The nastiest class. The general rule: "sometimes" is most often a race, timing or memory. Among hardware causes — long wires, power and ground: these are examined with an oscilloscope, not a debugger.
| Symptom | Cause | Fix | § |
|---|---|---|---|
| works only with the debugger | a race: the timing under the debugger is different | volatile, critical sections, waiting for flags |
3, 16 |
works at -O0, breaks at -O2 |
missing volatile or a race |
it is not a "compiler bug" | 3, 30 |
| after an hour — data corruption or a hang | an ordinary kernel function from an interrupt | only FromISR |
28 |
| the device crashes after a week | a memory leak | watch the minimum-ever free heap | 25 |
| memory melts away as tasks are created and deleted | the idle task does not get the CPU | give it time | 26 |
| tasks get created, then stop being created | the heap ran out | xPortGetMinimumEverFreeHeapSize |
25 |
| plenty is free, but nothing can be allocated | fragmentation | heap_4, create and delete less often |
25 |
| a high-priority task stalls for no reason | priority inversion | a mutex, held briefly | 30 |
| a task misses events | a binary semaphore with frequent events | a counting one or a queue | 28 |
| the task reaction lags by a millisecond | portYIELD_FROM_ISR forgotten |
add it | 28 |
| a critical section does not protect a variable | the variable is touched by a fast interrupt | different protection or below the threshold | 28, 30 |
| UART reception loses bytes while the display is in use | a long critical section | a mutex or a gatekeeper | 30 |
| UART bytes are lost under load | the buffer is full | a bigger buffer, a higher reader priority | 32 |
| data in the buffer gets corrupted | two writers or two readers | the single-writer, single-reader rule | 32 |
| notification values are lost | it holds only one | a queue | 31 |
| bounce gives a dozen interrupts | the button mechanics | debouncing | 7 |
| UART: the first byte is lost | the receiver is enabled later than the transmitter | the enabling order | 16 |
| UART: reception is silent after a stream | ORE not cleared |
read SR, then DR |
16 |
| UART: the end of a transmission is cut off | waited for TXE instead of TC |
TC before sleep |
16 |
| SPI: data is cut off at speed | CS released before BSY cleared |
wait for BSY |
18 |
| USB: works on one computer, not on another | the pull-up or the line quality | resistor; cable | 20 |
| USB: drops out after sleep | it requires activity | wake-up handling | 20 |
| USB: data is lost when sending fast | the host polls on a schedule | a buffer and sending when ready | 20 |
| a command over USB CDC is not recognized | the packet was compared with the command, but it arrived in pieces | a ring buffer and assembling lines in a loop | 20 |
| binary data over CDC is cut off | strncpy stopped at a zero byte |
copy bytes, not strings | 20 |
| a CDC reply is corrupted | the buffer was changed before the transfer finished | a separate buffer, send after USBD_OK |
20 |
| SPI flash works every other time on a breadboard | long wires at 20 MHz | lower the speed, shorter wires | 41 |
| I²C hangs forever | no timeouts; a wire came loose | timeouts everywhere | 17 |
the stack runs out on printf |
hundreds of bytes for formatting | headroom or drop printf |
26 |
| stack overflow in the timer daemon | printf or an array in a callback |
do not do that there | 29 |
| a zero-length file after power-off | no f_close/f_sync |
f_sync after writing |
42 |
| RTC time on F1 occasionally jumps by 18 hours | the counter was read in halves without a retry — the low half wrapped through 0xFFFF between reads | re-read the high half and retry | 12 |
| a counter from threads does not add up, differently every time | a race: ++ is not atomic, volatile does not help |
a mutex or atomic_fetch_add |
139 |
| a counter in shared memory counts wrong | no semaphore between processes | a POSIX or XSI semaphore | 137 |
| a signal "got lost" | standard signals are not queued | block them with a mask instead of losing them | 136 |
| a counter from two ESP32 tasks does not add up, although there is a mutex | the second core: the mutex does not protect against it | a portMUX_TYPE spinlock or __atomic_* |
152 |
| a clock input on a long wire catches extra edges | ringing of an unmatched line: the wave doubles at the end and reflects | a series resistor at the pin up to the line's characteristic impedance | 160 |
| I²C works on the bench, fails on the wires in the enclosure | the wire capacitance slows the R·C edge beyond the mode's limit | a smaller pull-up, shorter wires, standard mode or a bus buffer | 160 |
36.7 Hangs
| Symptom | Cause | Fix | § |
|---|---|---|---|
| hangs, but not completely: part of it works | a high-priority task without waiting | add blocking | 26 |
| alive, but does nothing | a deadlock of two mutexes | a single lock order | 30 |
stuck on xSemaphoreTake of its own mutex |
it took it twice | sort out the calls; a recursive one | 30 |
| all software timers stopped at once | a callback blocked | zero wait in callbacks | 29 |
| hangs without the debugger | SWO waits for readiness with the module disabled | check ITM_TCR_ITMENA |
21 |
| hangs waiting for RCC readiness | the crystal did not start, there is no timeout | a timeout; check the crystal | 6 |
| the handler waits on a semaphore | you cannot wait in an interrupt | TakeFromISR without waiting |
28 |
xTimerStart returns a failure |
the command queue is full or the daemon is stuck | see "all timers stopped" | 29 |
| task synchronization hangs | SetBits + WaitBits instead of Sync |
xEventGroupSync |
31 |
xMessageBufferReceive always returns zero |
the receive buffer is smaller than the message, which got stuck | receive buffer ≥ the maximum frame | 32 |
buffer Reset fails |
someone is waiting | stop the reader | 32 |
| flash erase "hung" | hundreds of milliseconds are normal | wait for BUSY with a timeout |
41 |
| the device does not sleep in tickless mode | a task wakes up more often than every two ticks | find it | 33 |
| tickless mode is on, but the current did not drop | configPRE_SLEEP_PROCESSING zeroes the argument before calling your function |
pass the time as is or go to sleep inside the function yourself | 33 |
after vTaskSuspend the task is dead |
only vTaskResume brings it back |
do not confuse it with waiting | 26 |
open of a FIFO for reading hangs |
there is not a single writer | O_NONBLOCK or open it for writing yourself |
137 |
cond_wait — sleeping forever |
called without the mutex held or without a condition-check loop | while (!ready) cond_wait under the mutex |
139 |
cat /dev/ringdev does not finish |
the driver's read does not return 0 on an empty buffer |
return 0 — end of file | 147 |
Task watchdog got triggered … IDLE1 |
a task on core 1 did not yield the CPU for longer than 5 s | vTaskDelay in the loop or subscribing to the watchdog |
152 |
Interrupt wdt timeout on CPU0 |
interrupts were disabled for longer than 300 ms: an overlong critical section | shorten the section | 152 |
36.8 Memory and HardFault
| Symptom | Cause | Fix | § |
|---|---|---|---|
PC in garbage, UNDEFINSTR |
the stack overwrote the return address | measure the stack; a trap | 35, 26 |
PRECISERR, BFAR is a small number |
a null pointer with an offset | look for NULL |
35 |
IMPRECISERR, PC points elsewhere |
the write went into the bus buffer | DISDEFWBUF while debugging |
35 |
UNALIGNED |
uint32_t * on a byte buffer |
memcpy or packing |
35, 3 |
HardFault in a handler with priority 0 |
a handler above the syscall threshold calls the kernel | priority ≥ the threshold | 7, 28 |
| the DMA buffer "is not filled" on F4 | the buffer is in CCM, DMA cannot reach it | ordinary SRAM | 9, 41 |
| DMA corrupts the stack | the local buffer disappeared along with the frame | a static buffer | 9 |
| data from DMA does not change in the code | no volatile |
add it | 9, 14 |
corruption with heap_5 |
the regions are not in ascending address order | sort them | 25 |
| a static stream buffer corrupts its neighbor | an array of N instead of N + 1 |
+ 1 |
32 |
FATFS declared locally |
the structure must live all the time | static | 42 |
| stack overflow on a sector buffer | 4 KB locally | a static buffer | 42 |
| the DMA buffer is not aligned | a 16/32-bit element at an odd address | __attribute__((aligned)) |
9, 42 |
| flash: wrote one thing, read another | writing without erasing: only 1→0 | erase the sector | 41 |
| the flash died after a month | 100000 cycles per sector; a log in a single sector | sector rotation | 41 |
floating WP#/HOLD# |
they pick up noise | pull them up to the supply | 41 |
| settings were lost after power-off in the middle of saving | the page was erased, but not written in time | a log over two pages | 11 |
| after a firmware update the settings "shifted" | the structure layout changed | a version field, defaults on mismatch | 11 |
| a settings write aborts with an error halfway | the block is larger than a page: the next one was not erased | write no more than a page, or a log | 11 |
| interrupts are delayed by tens of ms while saving | erasing Flash stalls instruction fetch (F1, single bank) | save at a quiet moment; an urgent handler in RAM | 11 |
Guru Meditation (Cache disabled but cached memory region accessed) while writing NVS or updating |
a handler with the ESP_INTR_FLAG_IRAM flag calls a function without IRAM_ATTR or reads a constant without DRAM_ATTR, while the cache is disabled during the write; ordinary handlers simply wait at that time |
everything such a handler touches goes into IRAM_ATTR and DRAM_ATTR, or remove the ESP_INTR_FLAG_IRAM flag |
150 |
malloc of 200 KB returned NULL with 300 KB free |
the ESP32 heap is made of banks, the largest contiguous chunk is smaller | split the buffer or MALLOC_CAP_* |
150 |
HardFault right after a call through a function pointer |
an even jump address: CFSR = 0x00020000 (INVSTATE) | take the function's address, not a number; decode CFSR, HFSR and the PC in the exception frame | 4б |
36.9 Power and sleep
| Symptom | Cause | Fix | § |
|---|---|---|---|
| everything is slow after Stop | the clock tree was not restored: HSI instead of PLL | restore the PLL after waking up | 10 |
| sleep current is higher than calculated | pins in an undefined state | analog mode or a pull | 10 |
| sleep current is tens of µA higher | a ~40 kΩ pull-up on a pin held to ground externally | remove the pull-up or invert the logic | 10 |
| the current in Sleep does not drop | peripherals are on | turn them off before sleep | 10, 33 |
| the current in Stop is as in Sleep | the debugger left the DBG_STOP bits in DBGMCU->CR; a reset does not clear them |
clear the bits or power-cycle | 10 |
| the chip resets on wake-up while an ammeter is in the supply | the 200 µA range is a 1 kΩ shunt, the supply drops below 2 V | a jumper in parallel with the meter | 10 |
| multimeter readings jump by tens of times | it averages over fractions of a second, the bursts get caught every other time | measure sleep and the burst separately, compute the average | 10 |
| the programmer does not see the chip after preparing for sleep | PA13 and PA14 went into analog mode | keep SWD in the mask; flash with connect under reset | 10 |
| it sleeps, but not for long (≤ 233 ms) | the SysTick limit |
the core at a lower frequency all the time, or your own sleep timer | 33 |
| woke up — the time drifted | your own sleep function without vTaskStepTick |
correct it | 33 |
| waking up too often eats the benefit | leaving Stop/Standby is not free | wake up less often | 10 |
| feeding the watchdog from an interrupt | the interrupt is alive, the program is dead — the watchdog stays quiet | feed it from the main loop | 12 |
| the reset cause flags lie | not cleared | RMVF after reading |
12 |
| the reset cause is always "button" | PINRSTF is set on almost any reset |
check it last | 12 |
| WWDG resets right after start | counter 0x40 — one tick before reset | start with 0x7F | 12 |
| WWDG resets when fed | it is fed before the counter drops to the window W |
feed inside the window | 12 |
| the RTC alarm wakes only once | the alarm flag is not cleared (ALRAF, on F1 ALRF): no new edge on EXTI 17 |
clear the flag in the handler | 12 |
| the RTC alarm handler is called endlessly | the EXTI 17 line flag is not cleared | EXTI->PR = EXTI_PR_PR17 |
12 |
| the F4 alarm is not written | the WPR keys were not entered or ALRAWF was not waited for |
0xCA, 0x53, then wait for ALRAWF |
12 |
| noise in the ADC/DAC | ground, supply, reference | layout; filter; VDDA |
14, 15 |
| an electronic load does not go down to zero | the DAC output buffer is enabled | BOFF1: the op-amp input is high-impedance |
15 |
| the ADC reference "floats" | VDDA from USB through a diode |
a regulator or an external reference | 14 |
| ADC readings drift along with the battery | the reference is VDDA, but the calculation assumed 3.3 V |
recalculate via VREFINT |
14 |
| internal ADC channels give zero | TSVREFE is not enabled |
CR2 on F1, ADC->CCR on F4 |
14 |
an RTC_DATA_ATTR variable was zeroed after esp_restart |
it survives only deep sleep, on a software reset the bootloader loads it again | for resets — RTC_NOINIT_ATTR and zeroing on ESP_RST_POWERON |
157 |
ESP32 starts from app_main after deep sleep |
leaving deep sleep is a reset | keep the state in RTC memory, check esp_sleep_get_wakeup_cause() |
157 |
| an ESP32 sensor drains the battery in weeks | a long Wi-Fi connection on every wake-up: 2 s of radio cost more than a day of sleep | wake up less often, a static IP, the router channel in RTC memory | 157 |
| ESP32 reboots as soon as it turns on the transmitter | a dip of the 3.3 V rail at the 240 mA peak: too little capacitance at the module, a long cable | 10 µF and 100 µF or more at the module, a low-dropout regulator, a short cable | 161 |
36.10 Hardware
| Symptom | Cause | Fix | § |
|---|---|---|---|
| DRC: a short near the microcontroller package | the trace from the decoupling capacitor runs diagonally across neighboring pins | the capacitor perpendicular to the pin row, with its near pin towards VDD |
130, 131 |
| the crystal does not start when cold | the capacitors do not match the CL of your crystal |
C1 = C2 = 2 × (CL − Cstray) |
130 |
| a power trace heats up | the width is not sized for the current, especially on an inner layer | width per IPC-2221: 1 A at 10 °C — 0.30 mm outside, 0.78 mm inside | 130 |
| DRC fails in the build, although there are no violations | --exit-code-violations also counts unrouted connections |
use the option when the board is fully routed | 131 |
| the DipTrace plugin ran, but the board did not change | the exchange file was not rewritten: DipTrace only watches for its change and does not read the exit code | rewrite the same plugin_exchange.xml by replacing it entirely |
132 |
| an object deleted by the DipTrace plugin stayed on the board | the node was removed from the XML | set Enabled="N" |
132 |
| distances in an inch-based exchange file drift in the third digit | four digits in inches is a step of 2.54 µm | compare with a tolerance of a couple of micrometers | 132 |
configparser: "File contains no section headers" on an Altium project |
.PrjPcb starts with a UTF-8 byte order mark |
read it with the utf-8-sig encoding |
133 |
a KiCad script does not open an Altium board, LoadBoard returned nothing |
the format is not guessed from the file | specify PCB_IO_MGR.ALTIUM_DESIGNER |
133 |
| after importing from Altium the net classes are 0.2 mm instead of 0.1 | the classes got KiCad settings, not the Altium rules | define the net classes again | 133 |
| KiCad does not open its own IPC-2581 or ODB++ | KiCad only writes these formats | transfer as a project or a netlist | 134 |
| after importing a netlist the parts have no packages | footprint names differ between programs | map packages by name, the rest by hand | 134 |
| the mechanical engineer got a board without parts | no 3D models are attached to the parts | attach models before exporting STEP | 134 |
| a crystal of the wrong frequency | 8 MHz on the board, 25 in the project | make them match | 6 |
| the system stayed on HSI | SW was checked instead of SWS |
wait for SWS |
6 |
| HSE was lost — the device stopped | no CSS | enable it, handle the NMI | 6 |
| UART: lines swapped | TX to TX |
TX → RX |
16 |
| Cyrillic text garbled in the terminal | the terminal encoding | UTF-8 on both sides | 16 |
| I²C: wrong address | 0x68 versus 0xD0 |
the 7-bit format with a shift | 17 |
I²C: ADDR is not cleared |
only SR1 was read |
SR1, then SR2 |
17 |
| I²C: works with one module, not with several | the modules' pull-ups add up: five of 4.7 kΩ give 940 Ω, more than 3 mA | keep the pull-ups on one module only | 17, 95 |
| I²C: works on the bench, fails on a cable | the cable capacitance stretches the edge: at 400 kHz with 2.2 kΩ — 1.71 m | lower speed, recalculate the pull-up, a buffer | 17, 95 |
| no common ground | the sensor is on its own board | connect them | 17, 19, 95 |
| an input glitches or heats up with a 5 V module | the module pulls the bus up to 5 V | remove the module's pull-ups or use a level shifter | 62, 95 |
| a thermistor shows −40 or +125 and does not change | an open or short in the divider: the code is beyond the table | check for out-of-table values, do not report them as temperature | 96 |
| SPI: garbage with several devices | two CS are active |
pull-ups on all CS |
18 |
| SPI: the display works, the flash does not | different modes on one bus | switch the mode | 18 |
| the SD card is silent on CMD0 | a wrong CRC7, too few clocks before the command or a speed above 400 kHz | CMD0 is 0x40 00 00 00 00 95, 74 clocks, prescaler 256 | 18 |
| bytes are swapped on the 74HC595 outputs | sent in direct order | the byte for the farthest register goes first | 18 |
| SWO on F407 prints garbage | the debugger has the 8 MHz crystal frequency instead of the 168 MHz core | the core frequency in the settings, ACPR = 83 for 2 MHz | 21 |
| SWO loses characters and slows the program down | byte by byte, a 2 MHz line passes 100000 characters per second | write words — 160000, print less | 21 |
| an extra key appears pressed on the keypad | three keys at the corners of a rectangle without diodes: 144 triples out of 560 | a diode on every key | 22 |
| the common pin of a display digit heats up | eight segments at 8 mA — 64 mA through one pin | a transistor as the digit switch | 22 |
| CAN without a transceiver | direct connection | a transceiver is mandatory | 19 |
| CAN: terminators | not two of 120 at the ends | exactly two, at the ends | 19 |
| USB: detected as unknown | descriptors | a protocol analyzer | 20 |
| USB: the cable | unshielded, thin | another cable | 20 |
| SPI flash: the mode | 0 or 3, not 1 and 2 | CPOL=CPHA |
41 |
| current leaks through a floating input | an input without a pull | pull it | 8, 10 |
| the relay switch burns out, ESP32 resets when the relay turns off | no diode across the coil: the spike breaks down the switch | a diode across the coil, cathode to the supply, TVS for remote loads | 161 |
| a MOSFET driven from an ESP32 pin heats up, the relay clicks uncertainly | the transistor is not logic-level: at 3.3 V it is not fully on | a low-threshold transistor or an NPN with a base resistor | 161 |
36.11 Communications and radio
| Symptom | Cause | Fix | § |
|---|---|---|---|
the Wi-Fi module answers ERROR to everything |
the wrong UART speed or the previous command has not finished | speed; wait for the response | 37 |
| the module response parser gets confused | an unsolicited message arrived between a command and its response | parse each line by its prefix | 37 |
| the broker disconnects every minute and a half | no PINGREQ more often than the keep-alive |
send it on a timer | 37 |
CONNACK with code 5 |
not authorized: someone else's or an empty token | each device has its own token | 37, 91 |
| the connection to the broker silently drops | the router removed the NAT entry while there was no traffic | keep-alive | 86 |
| the server cannot reach the device | the device is behind NAT | the device connects by itself | 86 |
| after the server moved, all devices are silent | the firmware has an address, not a name | the name in settings that can be changed over the air | 87, 91 |
| the server cannot come back up after a failure | all devices retry at once | a growing delay with random jitter | 88 |
| duplicate measurements in the database | a POST retry after a dropped connection |
PUT by number or an idempotency key |
88 |
| the request body is cut off on Cyrillic text | Content-Length in characters |
the length in bytes | 88 |
| the load balancer sends everything to one server | the server group has no shared zone | zone in upstream |
88 |
| RS-485 loses the last byte | the transmitter is turned off on TXE |
turn it off on TC |
89 |
| Modbus: CRC error | the CRC bytes are swapped | CRC least significant byte first | 89 |
| the CoAP server executed a command twice | the retry was sent with a new message ID | the same ID on a retry | 89 |
| Zigbee loses packets near Wi-Fi | the channel overlaps with Wi-Fi | channels 15, 20, 25, 26 | 89 |
| a LoRaWAN device goes silent for minutes | at SF12 the 1 % duty cycle ran out | send less often, a lower SF | 89, 93 |
SX1276: RegVersion reads as 0x00 or 0xFF |
wires, SPI mode, NSS |
check in order | 93 |
| SX1276 does not transmit, although the registers are written | the LoRa bit was not written in sleep mode | sleep → LoRa → wait | 93 |
| SX1276 is on the wrong frequency | Frf was computed in 32 bits |
compute it in uint64_t |
93 |
| LoRa packets do not arrive, no errors | different SF, bandwidth or sync word | compare the parameters on both sides | 93 |
| the network server silently does not see the device | the address or key is in the other byte order | in the frame — least significant byte first | 94 |
| an ABP device went silent after a reboot | the frame counter started from zero | keep the counter in non-volatile memory | 94 |
| commands do not reach a class A device | the RX1 window opens too late | a timer from the "transmission finished" event | 94 |
| the device's silence cannot be interpreted | there is no heartbeat message | an "I am alive" message at a set interval | 90 |
| the coverage map lies | the path loss model has no walls | walls in the model, a floor plan with materials | 92 |
ESP32: esp_netif is started, but there is no address |
the event loop is not created or NVS is not initialized | nvs_flash_init() and esp_event_loop_create_default() before the network driver |
156 |
| ESP32 cannot connect to Mosquitto 2 | without listener 1883 and allow_anonymous the broker only admits localhost |
configure a listener and client credentials | 156 |
| the ESP32 log has dates from 1970 | SNTP has not synchronized yet, there is no battery-backed clock | wait with esp_netif_sntp_sync_wait with a timeout, keep logs in UTC |
156 |
| ESP32 is not visible in a BLE scanner after the phone disconnects | advertising is not restarted on the disconnect event | restart advertising in the BLE_GAP_EVENT_DISCONNECT handler |
158 |
| only the first 20 bytes arrive over BLE | the default MTU is 23, the header is 3 bytes | an MTU exchange or sending in chunks | 158 |
36.12 If nothing fits
Rows that are not in the table are new pitfalls, and they also teach something. The order:
- Reduce to the minimum — a ten-line project.
- Read the registers — all that belong to the block, in the debugger.
- Compare with a known working one — an example from the pack, someone else's board, another chip.
- Oscilloscope on the line — what is on the wire, not what should be there.
- Write it down — symptom, cause, fix. A year from now it will save a day.
36.13 Summary
- Five questions before searching: does it build without warnings; does it reach
main; is it clocked; is the frequency right; is the interrupt enabled everywhere. - Three techniques: halve it, reduce to the minimum, read registers.
- "Works only with the debugger" — a race. "After an hour" — a kernel call
from the wrong place. "After a week" — a leak. "At
-O2" —volatile, a race or undefined behavior. - "The code is correct, but it does not work" — the block clock, in half of the cases.
- "Everything is off, but proportionally" — the frequency: crystal, PLL,
configCPU_CLOCK_HZ. - A hang with a partly alive device — a task without waiting; a device that is alive but completely dead — a deadlock.
- A
HardFaultcan always be read:PCfrom the frame, the cause fromCFSR. - Communications: the device connects by itself, retries with jitter and remembers its counters; radio parameters and byte order are checked on both sides.
Next: section 36а "Instruments on the bench" — the multimeter, the oscilloscope and the logic analyzer: how to look at what the debugger cannot see.