For weeks I had been debugging an I2C sensor the way you debug something you can't see: by changing one thing, reflashing, and squinting at whether the number that came back looked less wrong than before. The sensor would mostly work, then return zeros, then work again. My code was full of retries and delays I'd added on faith, none of which I could justify, all of which I was afraid to remove.
Then I bought a logic analyser. Not a nice one. One of the small clones of the Saleae Logic, eight channels, about a tenner, the kind you clip onto headers with little grabber leads. It speaks over USB to sigrok and PulseView on the laptop, and that combination turned out to be the single best money I have spent on this hobby.
seeing the wire instead of imagining it
The first capture was a revelation. I clipped onto SDA and SCL, set the trigger, ran one read, and there it was: the actual clock, the actual data, the start condition, the address byte, the ACK, the lot. PulseView even has an I2C decoder, so it doesn't just draw the squiggles, it annotates them. Start, Write 0x76, ACK, and so on, in plain text above the trace.
And immediately the problem was obvious in a way no amount of printf debugging had managed. My zeros weren't a sensor fault. They were a NAK. The device was not acknowledging its own address on those reads, which meant it wasn't ready, which meant my startup delay after power-on was too short for the cold device but fine once it had warmed up. The intermittency wasn't random at all. It was thermal, and the analyser handed me that conclusion on a plate.
the things I'd been getting wrong
A few sins surfaced once I could actually watch the bus.
- My pull-up resistors were too weak. The rising edges on SCL were lazy, sloping curves rather than crisp steps, and at the clock speed I'd chosen the line wasn't reaching a clean logic high in time. Swapping 10k for 4.7k pull-ups sharpened them up nicely.
- I was clocking faster than the cheap jumper wires liked. Dropping from 400kHz to 100kHz removed a whole class of marginal reads I'd been papering over with retries.
- One of my "delays added on faith" was doing nothing useful and one was genuinely load-bearing. Without being able to see the bus, I had no way to tell which. Now I did, so I could delete the dead one and keep the real one with a comment explaining why.
The retries came out. The unexplained delays came out. The code got shorter and, for the first time, I understood every line that remained.
get one before you need one
The wider point is about feedback loops. I had spent weeks in a loop where my only signal was "did the right number come back," which is an almost uselessly low-bandwidth way to debug a serial protocol. The analyser changed the signal to "here is exactly what travelled down the wire, byte for byte, with timing." That is a different kind of debugging entirely. You stop guessing and start reading.
If you do anything with I2C, SPI, UART or even just poking at unknown signals, get a cheap analyser before you think you need one. It costs less than the parts you'll otherwise destroy out of frustration, and the first time you watch a transaction decode itself in front of you, the whole field of embedded work feels a little less like sorcery and a little more like engineering.