Lab 3.1 — I²C Scan + Saleae Decode
← Course 2 syllabus · Module 3 · Prev: « Lab 2.3 · Next: Lab 3.2 »
Goal
Bring up the I²C bus on the STM32 and prove it works before trusting it with real data. You will wire both Module 3 peripherals — the MCP4725 DAC and the ADS1115 ADC — onto one shared two-wire bus, write a firmware address scanner that walks every 7-bit address and reports which ones ACK, and then put the Saleae Logic 8 on the same wires to decode the START/address/R‑W/ACK/STOP framing byte-for-byte. Being able to bring up an unfamiliar I²C device, confirm its address, and read the bus on a logic analyzer is a daily skill for firmware and DSP embedded engineers — almost every sensor, DAC, ADC, and codec you will ever touch speaks I²C or SPI, and “it doesn’t respond” is the most common bring-up failure.
Recommended reading
- Kuo Ch. 1 — the real-time DSP system context: converters and peripherals hang off the processor’s serial buses; understand where I²C sits in a DAQ system. Light read.
- Lyons Ch. 11 (binary data formats / fixed-point representation) — a light read on how multi-bit converter values are packed into bytes and register pairs; you’ll unpack these in Labs 3.2 and 3.4.
- The NUCLEO-L476RG / STM32L476 reference manual, the I2C peripheral chapter, and the MCP4725 and ADS1115 datasheets — read the device addressing section of each in full (this lab is entirely about addressing).
- No new Course 1 dependency; I²C is a protocol, not a theorem.
Equipment & parts
- STM32 Nucleo-64 (NUCLEO-L476RG) + USB cable to the host (ST-LINK VCP for the console).
- MCP4725 12-bit I²C DAC breakout.
- ADS1115 16-bit I²C ADC breakout.
- Saleae Logic 8 logic analyzer + Logic 2 software.
- Breadboard, jumper wires, and two ~4.7 kΩ resistors for the SDA/SCL pull-ups (from the kit).
- Both breakouts and the Nucleo run at 3.3 V for this lab (tie every VDD to the Nucleo’s 3V3 pin so the whole bus is one logic level — 5 V level-shifting is deferred to Lab 3.5).
Safety & don’t-break-it
- Keep the whole bus at 3.3 V for this lab. Power both breakouts from the Nucleo’s 3V3 pin, not 5 V. The STM32’s SDA/SCL are not 5 V tolerant here, and a 5 V idle-high bus would push current into the pins through the pull-ups. (Mixing a 5 V device onto this bus is exactly what Lab 3.5 is for.)
- You need pull-up resistors, and only one set. I²C SDA and SCL are open-drain: the bus can only pull low, so external pull-ups to 3V3 create the high level. Many breakouts already have on-board pull-ups (often 10 kΩ). Two devices’ pull-ups in parallel is usually fine; adding your own ~4.7 kΩ on top lowers the effective resistance. If the bus won’t idle high or edges look slow, check how many pull-ups are actually on the wire before adding more.
- Common ground is mandatory. The Nucleo GND, both breakout GNDs, and the Saleae GND must all tie together. An I²C bus without a shared ground reads as garbage or as a dead bus.
- Don’t hot-swap chips on a powered bus. Insert/remove breakouts with the Nucleo unpowered. Double-check VDD and GND are not swapped on each breakout before applying power — a reversed rail can kill the part.
- The Saleae is not a scope. Keep its inputs within ~5 V and share ground with the DUT. Set its logic threshold to 3.3 V so it interprets levels correctly.
Background
I²C is a two-wire, open-drain, multi-drop bus: SDA (data) and SCL (clock), each idling high through a pull-up. The master (the STM32) drives all clocking. A transaction is framed as:
- START — SDA falls while SCL is high.
- Address frame — 7 address bits, MSB first, then an R/W̄ bit (0 = write, 1 = read). The addressed slave pulls SDA low for the 9th clock: an ACK. No slave present ⇒ SDA stays high ⇒ NACK.
- Data bytes — 8 bits each, every byte followed by an ACK/NACK.
- STOP — SDA rises while SCL is high.
An I²C scan exploits the address-frame ACK: for each 7-bit address \(0x08\)–\(0x77\), the master issues START + (address, write) and checks the 9th-clock level. An ACK means “something lives here.” In HAL, HAL_I2C_IsDeviceReady() does exactly this — one address probe with a short timeout.
The wire address on the bus is the 8-bit byte (addr << 1) | R/W̄. This is the classic 7-bit-vs-8-bit confusion: a datasheet “0x60” 7-bit address appears on the Saleae as 0xC0 for a write or 0xC1 for a read. Keep the two representations straight.
Expected addresses on this bus (verify — do not assume):
- MCP4725 — 7-bit 0x60–0x67 depending on the A0 pin and factory variant; HiLetgo boards are commonly 0x60, some 0x62.
- ADS1115 — 7-bit set by the ADDR pin: 0x48 (ADDR→GND, default), 0x49 (→VDD), 0x4A (→SDA), 0x4B (→SCL).
Procedure
Part A — Wire the bus (power off).
- On the breadboard, create an SDA rail and an SCL rail. Wire the Nucleo PB9 = SDA and PB8 = SCL (I2C1) to those rails. Tie a 3V3 rail and a GND rail from the Nucleo.
- Connect MCP4725: VDD→3V3, GND→GND, SDA→SDA rail, SCL→SCL rail. Leave its A0 pin as the board default for now (note which — it sets the address).
- Connect ADS1115: VDD→3V3, GND→GND, SDA→SDA rail, SCL→SCL rail, ADDR→GND (address 0x48).
- If the bus does not already have pull-ups, add ~4.7 kΩ from SDA→3V3 and SCL→3V3. (Check the breakouts first — see Safety.)
- Clip the Saleae onto the bus: CH0→SDA, CH1→SCL, and a Saleae GND→the common GND rail.
Part B — Firmware I²C scan.
- In STM32CubeIDE, enable I2C1 on PB8/PB9 (Standard-mode 100 kHz to start), and USART2 for the console (
printfover the ST-LINK VCP). Generate the project. - Write a scan loop that probes every address and prints the hits:
/* Illustrative only — you write the real firmware.
Walk the 7-bit address space and report every device that ACKs. */
for (uint8_t addr = 0x08; addr <= 0x77; addr++) {
/* HAL wants the address left-shifted into the 8-bit field. */
if (HAL_I2C_IsDeviceReady(&hi2c1, (uint16_t)(addr << 1),
1 /*trials*/, 5 /*ms timeout*/) == HAL_OK) {
printf("Found device at 0x%02X\r\n", addr);
}
}
printf("Scan complete.\r\n");- Open a serial console on the VCP (115200 8N1). Flash and run. You should see two hits — expect ~0x60 (DAC) and 0x48 (ADC). Record the actual addresses.
Part C — Decode on the Saleae.
- In Logic 2, set the digital threshold to 3.3 V, sampling rate a few MS/s, and add an I²C analyzer on CH0 (SDA) / CH1 (SCL).
- Capture while the scan runs. Zoom to a single probed address and read the decoded frame: START, the 7-bit address + R/W̄ bit, the ACK/NACK on the 9th clock, and the STOP.
- Find an address that NACKs (no device) and one that ACKs (the DAC or ADC). Confirm the ACK bit is SDA held low by the slave on the 9th clock. Confirm the on-wire byte equals
(addr<<1)— e.g. a 0x60 device shows 0xC0 for the write frame.
Deliverable & expected results
Capture a Logic 2 session (captures/lab-3-1-i2c-scan.sal) and a short note recording: the two addresses your scan found, one annotated ACK frame and one NACK frame, and the on-wire 8-bit byte for a write to the DAC.
| Quantity | Predicted | Measured |
|---|---|---|
| MCP4725 7-bit address | 0x60 (verify — may be 0x62) | … |
| ADS1115 7-bit address (ADDR→GND) | 0x48 | … |
| On-wire byte, write to 0x60 | 0xC0 | … |
| On-wire byte, read from 0x48 | 0x91 | … |
| ACK level on 9th clock (device present) | SDA pulled low (0) | … |
| Number of devices found by scan | 2 | … |
Analysis & reconciliation
The two firmware hits and the two ACKing addresses on the Saleae must agree — that is the whole point of cross-checking a software scan against a bus decode. If the DAC address is 0x62 rather than 0x60, that’s a legitimate board variant, not a bug; record what your part actually is and use it in Lab 3.2. If a device doesn’t ACK, work the usual list: swapped SDA/SCL, missing common ground, no pull-ups (bus never idles high), or wrong HAL_I2C_IsDeviceReady shift (passing the 7-bit value un-shifted probes the wrong address). If edges look badly rounded on the Saleae, your pull-ups are too weak (resistance too high) for the bus capacitance — you’ll quantify this rise-time effect in Lab 3.5.
Going further
- Re-run the scan at 400 kHz (fast mode) and compare the SCL period and edge shape on the Saleae to the 100 kHz capture.
- Change the ADS1115 ADDR pin to VDD, SDA, or SCL and confirm the scanned address moves to 0x49 / 0x4A / 0x4B as the datasheet predicts.
- Add the MCP4725 A0 strap change (if your board exposes it) and watch the DAC address shift — good proof you understand the addressing scheme before you start writing real data in the next lab.