Bridge two voltage worlds safely. You will use the Coliao bi-directional I²C level shifter to connect the 3.3 V STM32 to a device powered at 5 V, wiring the high-voltage (HV) and low-voltage (LV) reference pins, sharing a common ground, and placing the pull-ups correctly on each side. Then you will watch both the LV and HV sides of the bus simultaneously on the Saleae to see the same I²C transaction at two different logic levels, and study the rise-time / edge effects of the open-drain bus. Knowing when you need level shifting (and when a 5 V-tolerant pin makes it unnecessary), and why driving 5 V straight into a 3.3 V part destroys it, is core mixed-signal survival knowledge — this is the lab that keeps you from silently killing chips for the rest of the course.
Recommended reading
Kuo Ch. 1 — the system view: multi-voltage subsystems around a processor and the interfacing that stitches them together. Light read.
The Coliao / generic MOSFET I²C level-converter application note (the classic bidirectional single-N-MOSFET-per-line topology) — understand how one FET plus the two pull-ups makes the level translation bidirectional without a direction pin.
The STM32L476 datasheet pin table — find which pins are 5 V tolerant (FT) and which are not (all analog / VDDA pins are not). This is what tells you when you can skip the shifter.
Re-skim Lab 3.1 (open-drain bus, pull-ups) — this lab is that bus, split across two voltage domains.
Equipment & parts
STM32 Nucleo-64 (NUCLEO-L476RG) — the 3.3 V (LV) side.
A 5 V-powered I²C device for the HV side — e.g. the MCP4725 run at VDD = 5 V (its output/logic then live in the 5 V world), or the ADS1115 at 5 V.
Saleae Logic 8 (to watch LV and HV sides at once).
Pull-up resistors (~4.7 kΩ) for each side (LV pull-ups to 3.3 V, HV pull-ups to 5 V) if the boards don’t already provide them.
A stable 5 V rail (bench supply or the MB102 module’s 5 V) and the Nucleo 3V3; breadboard, jumpers.
Wiring & bench setup
The signal chain: the 3.3 V Nucleo drives the LV side of the Coliao shifter, the shifter’s HV side carries the same bus at 5 V to the 5 V-powered device, and the Saleae watches both domains at once.
flowchart LR MCU["NUCLEO-L476RG<br/>3.3 V LV domain<br/>SCL = PB8 (D15)<br/>SDA = PB9 (D14)"] SHIFT["Coliao shifter<br/>LV ref = 3V3<br/>HV ref = 5 V"] DEV["MCP4725 or ADS1115<br/>VDD = 5 V"] PSU["MB102 supply<br/>jumper → 5 V"] MCU -- "LV SCL / SDA + 3V3 ref" --> SHIFT SHIFT -- "HV SCL / SDA at 5 V" --> DEV PSU -- "5 V rail + common GND" --> SHIFT SAL["Saleae Logic 8<br/>CH0/CH1 = LV, CH2/CH3 = HV"] SAL -. "taps both domains" .-> SHIFT
flowchart LR
MCU["NUCLEO-L476RG<br/>3.3 V LV domain<br/>SCL = PB8 (D15)<br/>SDA = PB9 (D14)"]
SHIFT["Coliao shifter<br/>LV ref = 3V3<br/>HV ref = 5 V"]
DEV["MCP4725 or ADS1115<br/>VDD = 5 V"]
PSU["MB102 supply<br/>jumper → 5 V"]
MCU -- "LV SCL / SDA + 3V3 ref" --> SHIFT
SHIFT -- "HV SCL / SDA at 5 V" --> DEV
PSU -- "5 V rail + common GND" --> SHIFT
SAL["Saleae Logic 8<br/>CH0/CH1 = LV, CH2/CH3 = HV"]
SAL -. "taps both domains" .-> SHIFT
Pin map (every wire; Coliao pins per its silkscreen — LV/HV refs, LV1–LV4/HV1–HV4 channel pairs):
Setup gotchas: the MB102 jumper on the side feeding the HV rail must sit at 5 V (its other position is 3.3 V — that would silently give you a no-shift lab); power the MB102 from its own barrel/USB input, never from the Nucleo. Both supplies must be present: the shifter only translates when the LV ref (3.3 V) and HV ref (5 V) are both up — with one missing, that side’s lines float at an undefined level. Coliao modules ship with on-board pull-ups on both sides (commonly 10 kΩ); check yours before adding the external 4.7 kΩ sets, and keep each added pull-up referenced to its own side’s rail.
Safety & don’t-break-it
This lab exists because 5 V into a 3.3 V pin destroys the part. The STM32’s non-5V-tolerant pins (and the 3.3 V-powered MCP4725/ADS1115) clamp inputs at roughly VDD + 0.3 V. A 5 V logic high sits ~1.4 V above that clamp; the input protection diode conducts, dumps current into the rail, and the pin (or chip) fails — sometimes immediately, sometimes as a slow degradation. Never tie a 5 V bus line straight to a 3.3 V SDA/SCL pin. The shifter’s whole job is to prevent this.
Set the reference pins correctly and in the right order. The LV ref pin gets 3.3 V, the HV ref pin gets 5 V. Power the LV side / apply LV ref appropriately per the module’s guidance; a common safe practice is to bring up grounds and references before signals. Swapping HV and LV references defeats the translation and can expose the 3.3 V side to 5 V.
Common ground is non-negotiable across the Nucleo, the shifter, the 5 V device, and the Saleae. Different grounds = wrong thresholds = damaged parts.
Pull-ups belong on each side, referenced to that side’s rail. LV lines pull up to 3.3 V; HV lines pull up to 5 V. The level shifter needs pull-ups on both sides to function — the FET plus the two pull-ups is the translator. Don’t put a single pull-up spanning the domains.
Keep the Saleae inputs ≤ ~5 V and set its threshold appropriately per channel group (3.3 V logic on the LV probes, 5 V on the HV probes). Share its ground.
Project & environment setup
Firmware — reuse the Module 3 project (firmware/m3-mixed/, created in Lab 3.1) unchanged — this lab re-runs the Lab 3.1 address-scan firmware across the shifter. Confirm the .ioc still has:
CubeMX page
Setting
Connectivity → I2C1
I2C mode, Standard 100 kHz for bring-up (the 400 kHz stress test is in Going further); PB8/PB9
Connectivity → USART2
Asynchronous, 115200 8-N-1 (VCP console for the scan printout)
(Optional) scope shot of the HV rising edge for analog \(t_r\)
labs/lab-3-5/captures/hv-rising-edge.png
Background
I²C lines are open-drain: a device or the pull-up sets the level; nothing actively drives high. The standard bidirectional level converter puts one N-channel MOSFET on each line, with its gate tied to the lower rail (LV, 3.3 V), source on the LV side, drain on the HV side, and a pull-up on each side to its own rail. This one-FET cell translates in both directions automatically:
LV side pulled low (STM32 drives 0): the FET’s source goes low; with the gate at 3.3 V, \(V_{GS} = 3.3\text{ V} > V_{th}\), the FET turns on and pulls the HV side low too. Low propagates LV → HV.
HV side pulled low (5 V device drives 0): the FET’s body diode conducts to pull the source down, the FET then turns on fully, and the LV side goes low. Low propagates HV → LV.
Both idle high: each side is pulled to its own rail (3.3 V and 5 V) by its own pull-up; the FET is off. No conflict, no over-voltage on the LV side.
So the LV side never sees more than 3.3 V, and the HV side swings the full 0–5 V — the same logical bus at two physical levels.
Rise-time and edges. Because only the pull-up restores the high level, the rising edge is an RC exponential set by the pull-up resistance \(R\) and the total line capacitance \(C\) (wires + pin + FET):
Weaker pull-ups (larger \(R\)) give slower rise; more capacitance (long breadboard wires, many devices) slows it further. The FET adds a little extra capacitance, so shifted lines usually show visibly slower rising edges than a single-domain bus. If \(t_r\) grows toward the SCL period, the receiver may not see a clean high — the fix is stronger pull-ups (smaller \(R\)) or a slower bus. The I²C spec caps \(t_r\) at 1000 ns (standard) / 300 ns (fast). You’ll see falling edges stay sharp (actively driven low) while rising edges are the soft, RC-limited ones.
When you don’t need a shifter: if the STM32 pin is 5 V-tolerant (FT)and the logic-high threshold math works (a 5 V device’s \(V_{OH}\)/\(V_{OL}\) are read correctly by the 3.3 V part, and vice versa), you can sometimes share a bus directly — but not on analog/VDDA pins and not when the 3.3 V part’s high (~3.3 V) fails to reach the 5 V receiver’s \(V_{IH}\). When in doubt, shift.
Procedure
Part A — Wire the two domains (power off).
LV side: Nucleo PB9 = SDA, PB8 = SCL, 3V3, GND to the shifter’s LV channels and LV ref = 3V3 (per Wiring & bench setup: SCL→LV1, SDA→LV2). Add LV pull-ups (~4.7 kΩ) to 3.3 V if needed.
HV side: the shifter’s HV channels → the 5 V device’s SDA/SCL; HV ref = 5 V; the device’s VDD = 5 V. Add HV pull-ups (~4.7 kΩ) to 5 V if needed.
Tie all grounds together (Nucleo, shifter, 5 V device, and — shortly — the Saleae).
Saleae: put two probes on the LV SDA/SCL and two on the HV SDA/SCL, plus a Saleae GND to the common ground.
Part B — Bring up and scan across the shifter.
Power the 3.3 V side and the 5 V side. In Logic 2, set the LV-probe threshold to 3.3 V logic and the HV-probe threshold to 5 V logic; add an I²C analyzer on the LV pair.
Run the Lab 3.1 scan firmware. Confirm the 5 V device still ACKs at its expected address through the shifter — proof the translation works end to end.
Part C — See both sides at once.
Capture a transaction. Overlay the LV-SDA and HV-SDA traces: they carry the same bit pattern but at 3.3 V vs 5 V amplitude. Confirm START/address/ACK/STOP line up in time on both sides.
Zoom a rising edge on both sides. Measure the 10–90% rise time on the HV side (Saleae timing, or the scope if you want a real analog \(t_r\)). Compare falling (sharp) vs rising (RC-soft) edges.
Part D — The forbidden experiment, done safely (optional, on a sacrificial part only).
Reason about, don’t perform on a good chip: if you tied HV SDA (5 V) straight to a 3.3 V pin, the clamp diode would conduct and the current would be limited only by the pull-up — enough to degrade or kill the pin. If you want to see it, do it only on a scrap 3.3 V part you’re willing to lose, with a current-limited supply, and watch the 3V3 rail get pulled up by the clamp. Otherwise just record the reasoning.
Deliverable & expected results
A Logic 2 capture (labs/lab-3-5/captures/level-shift.sal) showing the same I²C frame on the LV (3.3 V) and HV (5 V) sides, an annotated rising edge with its rise time, and a note stating when the shifter is required vs. when a 5 V-tolerant pin lets you skip it. Predicted amplitudes are the rail voltages; \(t_r\) depends on your actual \(R\) and wiring \(C\).
The two Saleae domains should be bit-for-bit identical in time and differ only in amplitude — that’s the definition of a working translator. Compute the expected rising \(t_r \approx 2.2RC\) from your pull-up value and an estimated ~100 pF of line + FET capacitance, and compare to the measured HV edge; if it’s much slower than predicted, your pull-ups are too weak or your wires too long/capacitive (drop \(R\) or shorten the bus). Confirm the LV side never exceeds ~3.3 V on any edge — if it overshoots toward 5 V, a pull-up is mis-referenced or the FET cell is wired wrong, and you’re one mistake from the failure this lab exists to prevent. Reconcile the ACK: the fact that a 5 V-powered device answers a 3.3 V master proves the low-propagation path works in both directions.
Cross-platform ports & language variants
See the syllabus Implementation tracks for the framing; this is the level-shifter-specific version. Unlike the other labs there is no code or GPU angle here — it’s a hardware-transfer lesson, and it transfers verbatim.
The key point: the Raspberry Pi 5 and Jetson Orin Nano GPIO are also 3.3 V and not 5 V tolerant, exactly like the STM32’s I²C pins. So for any 5 V I²C device on a Pi or Jetson, the identical single-N-FET-per-line bidirectional shifter recipe applies unchanged — same LV/HV references, same pull-ups on each side, same open-drain RC rise-time physics (\(t_r \approx 2.2\,RC\)). The 3.3 V-domain survival rule and the shifter wiring carry straight from the MCU to the SBC, with the only difference being which 3.3 V host sits on the LV side. The SBC world has the same mixed-voltage constraint as the MCU world; the fix is the same circuit regardless of host.
Jetson Orin Nano — bench variant. Prove it on the bench in five minutes: swap the Nucleo’s four LV-side wires for the Jetson’s (pin 1 → LV ref + LV rail, pin 6 → common ground, pin 3 → LV2/SDA, pin 5 → LV1/SCL — the Lab 3.1 Jetson hookup, landing on the shifter instead of the bus rails), keep the HV side and the Saleae exactly as wired, and run i2cdetect -y -r 7. The 5 V-powered device ACKs through the shifter, and the two-domain capture looks identical to the STM32 run — same frames at 3.3 V and 5 V. One caution unique to the SBC: the Jetson header’s own pull-ups join the LV side’s total, so recount the pull-ups if the LV edges change shape. (Pi 5: bus 1, and its strong 1.8 kΩ on-board pull-ups make the LV rising edges visibly faster — a nice accidental demonstration of \(t_r \propto R\).)
Going further
Swap the ~4.7 kΩ pull-ups for 2.2 kΩ and 10 kΩ and measure how \(t_r\) scales — a direct \(t_r \propto R\) check, and the intuition behind “stronger pull-ups for faster/longer buses.”
Model the FET cell in LTspice (the LTspice workflow): schematic hardware/lab-3-5.asc — an NMOS (e.g. the built-in BSS138 if your model library has it, or any logic-level NMOS with \(V_{th}\!\approx\!1.5\) V), gate to the 3.3 V rail, source to an LV node with a pull-up to 3.3 V, drain to an HV node with a pull-up to 5 V, and ~50–100 pF from each node to ground. Drive the LV node low with a PULSE-controlled switch and .tran: watch the HV node follow low and both sides rise on their own RCs; then move the pulse source to the HV side and watch the low propagate the other way (the body diode then the channel). Overlay the simulated rise times on the measured Part C edges — the same three-way hand/sim/bench reconciliation as the analog labs.
Push the bus to 400 kHz through the shifter and find where the RC rise time starts eating the high time (the edge that eventually breaks fast mode).
Identify a genuinely 5 V-tolerant STM32 digital pin from the datasheet, wire a 5 V digital signal to it directly (not an analog pin!), and confirm it reads correctly without the shifter — the practical “when you can skip it” case.
Carry this setup forward: any 5 V sensor you add in later modules now has a known-good interfacing recipe.