A constant-current driver does not create missing voltage. In this experiment, two red light-emitting diodes (LEDs) are controlled independently by a Texas Instruments TLC5916IN integrated circuit (IC). A Pico sends the on/off commands; a separate adjustable supply provides the LED energy. Measure the current and the voltage left across each sink output while reducing that supply. The useful result is a table showing where your particular assembly stops holding approximately the same current, rather than a brightness demonstration.

This is a prepared low-voltage experiment, not a tested instrument. The complete program and its fault handling have host checks. No physical assembly, target firmware build, Pico execution, measured current curve or guaranteed regulation boundary is claimed. The program uses normal mode after a cold power-up; it does not program current gain or read the chip's special-mode error bits.

Separate the command, current and headroom

A current sink takes current from its output towards ground. Here, each path is supply → sense resistor → LED → TLC5916 output → ground. Each LED has its own path; the two LEDs are not connected in series. The Pico's general-purpose input/output (GPIO) pins carry logic commands and supply neither LED current.

Headroom is the voltage remaining across the sink after the LED and sense resistor have used part of the supply. More headroom can mean more heat in the driver; too little can mean less current. An illuminated LED is not evidence that current is regulated, and a successfully transmitted byte is not evidence that a physical output reached the requested state. Keep three separate observations in your notes: commanded mask, measured current and measured output-to-ground voltage.

Units used below are volts (V), milliamperes (mA), microseconds (µs), ohms (Ω), kilohms (kΩ), nanofarads (nF), watts (W), milliwatts (mW) and millivolts (mV). One milliampere is one thousandth of an ampere. A supply's displayed current limit is an experimental constraint, not a substitute for checking wiring.

Exact parts and a small current budget

PartValue and purpose
U1Texas Instruments TLC5916IN, N package: 16-pin plastic dual in-line package (PDIP). Use the exact top-view pin drawing; TLC59116 is a different device.
ControllerOriginal Raspberry Pi Pico with RP2040; MicroPython 1.26.0. Its 3V3_OUT pin supplies U1's 3.3 V logic rail. The LED rail is separate.
D1 and D2Two Kingbright WP7113ID bare red 5 mm LEDs, not the WP7113ID5V or 14V versions with a built-in resistor. Check anode/cathode against the current package drawing.
R11.8 kΩ, 1%, at least 0.25 W, from R-EXT to ground; sets the shared nominal current scale.
R2 and R310 Ω each, 1%, at least 0.25 W. Separate high-side current-sense resistors, not the main current-setting elements.
R4 and C110 kΩ output-enable pull-up and 100 nF ceramic supply bypass directly beside U1.
Bench supply and meterAdjustable LED rail limited to 0–5 V and 30 mA total, plus a floating handheld digital multimeter. Work near room temperature; this is not a thermal qualification.

The current TI datasheet lists 3.0–5.5 V for the logic supply. Keep this fixture at 3.3 V, so its input thresholds and Pico outputs share the same domain. Do not power U1's logic at 5 V and assume a 3.3 V GPIO HIGH remains a qualified input. The 7 V absolute logic-supply rating is a stress boundary, not a usable supply setting. Likewise, the LED's 30 mA absolute continuous-current rating is not our target current.

At the default power-up gain, the approximate setting equation gives:

I_nominal = (1.25 V / R_EXT) × 15
          = 18.75 / 1800 A = 10.42 mA per enabled channel
R_EXT tolerance alone: 10.31–10.52 mA for 1.8 kΩ ±1%
V_sense ≈ 0.01042 A × 10 Ω = 0.1042 V
P_sense ≈ I²R = 0.00109 W per resistor

The resistor-only range is not the chip's complete current accuracy. Do not apply an error specification tested at another current and output voltage as a universal guarantee here. Default high-current gain is intended for the 10–120 mA setting range; 1.8 kΩ keeps the nominal setting above its lower end even with the stated resistor tolerance. Two nominal channels consume about 20.8 mA from VLED, leaving room below the chosen 30 mA supply limit.

Wire two independent current paths

Original Pico GP2 to SDI pin 2, GP3 to CLK pin 3, GP4 to LE pin 4 and GP5 to OE pin 13 of TLC5916IN. Two separate WP7113ID LED paths each have a 10 ohm high-side sense resistor. Logic is 3.3 volts; the adjustable LED rail shares ground.
Original exact-pin functional schematic, not a physical breadboard placement. Each LED has its own sense resistor and sink output. Open the full-size drawing or download the editable Scalable Vector Graphics (SVG) file.
ConnectionExact destination
Pico GP2 / physical 4U1 SDI, serial data input, pin 2
Pico GP3 / physical 5U1 CLK, shift clock, pin 3
Pico GP4 / physical 6U1 LE, latch enable, pin 4
Pico GP5 / physical 7U1 OE, active-low output enable, pin 13; R4 pulls it to 3.3 V while the Pico is not driving it
Pico 3V3_OUT / physical 36U1 VDD logic supply pin 16 and C1; C1 returns to U1 ground
Pico GND / physical 38U1 ground pin 1, R1 return, C1 return and bench supply negative
U1 R-EXT pin 15R1 to common ground
U1 OUT0 pin 5 and OUT1 pin 6Separate LED cathodes. Each anode connects through its own 10 Ω resistor to VLED positive.
UnusedOUT2–OUT7, pins 7–12, remain open and are commanded off. SDO, serial data output, pin 14 remains open; it is not an input.

Keep the supply loop and bypass short. Place R1 close to U1, and put no capacitor on R-EXT. Common ground joins the supplies' returns; it does not join their positive rails. Never connect VLED to the Pico's 3.3 V rail, a GPIO or the universal serial bus (USB) power connector. The circuit supplies U1 from the Pico's regulated logic output, so a cold start means turning off both the USB-powered Pico and VLED, then starting the Pico/logic with VLED still off.

With power removed, verify each LED's polarity and resistance paths, and check that VLED has no unintended connection to the logic rail. Start the bench supply at zero output. Use its voltage setting and current limit independently; entering current limit changes the intended experiment and must be recorded.

Measure voltage headroom without inventing a knee

After a cold power-up, run the program below and select 1 to enable only OUT0. Bring VLED slowly towards 3.3 V with the 30 mA limit still set. If the supply limits or the readings contradict the drawing, request 0, turn VLED off and inspect the fixture. Do not increase the current limit to force a result.

  1. Measure the actual VLED rail relative to common ground.
  2. For current, put a floating handheld meter directly across R2: red lead at its VLED end, black common (COM) lead at its LED-anode end. COM is not connected to ground during this reading. Record the resistor voltage in millivolts. Do not duplicate this connection using a ground-referenced oscilloscope probe whose ground clip would short the node.
  3. Move the meter back to a voltage measurement with COM at common ground. Measure OUT0 at the LED cathode. This is the actual sink headroom.
  4. Record the selected mask, temperature, supply state and measurement order. These are sequential readings, not simultaneous samples.
I_LED = V_sense / R_sense
V_LED_forward = VLED − V_sense − V_OUT
P_sink_channel = I_LED × V_OUT

For a worked arithmetic example only, suppose VLED is 3.300 V, the sense reading is 104.17 mV, R2 is 10.00 Ω and OUT0 is 1.2958 V. The inferred current is 10.417 mA, the inferred LED forward drop is about 1.900 V, and that sink dissipates about 13.5 mW. These numbers are calculated examples; they were not collected from a board.

Reduce VLED in steps, waiting for stable readings at each point. A useful starting sweep is 3.3, 3.0, 2.8, 2.6, 2.4 and 2.2 V, followed by a return sweep. Stop wherever the fixture no longer gives useful readings. Record a current decrease as a measurement; do not silently replace it with the nominal setting. Repeat on OUT1 with mask 2, then compare both channels with mask 3. A difference may come from LED forward voltage, wiring, current matching, heating or meter uncertainty; the command byte alone cannot distinguish them.

At only 104 mV, meter offset and resolution matter. Estimate uncertainty from the meter's stated voltage accuracy and the measured resistor tolerance. A simple worst-case relative current bound is approximately relative voltage error plus relative resistance error. A percentage change smaller than that bound is not a demonstrated regulation change. Measuring voltage directly across the sense resistor avoids subtracting two much larger rail voltages.

For planning, assume a 1.90 V LED drop and the nominal 0.104 V sense drop. At VLED = 2.2 V, only about 0.196 V remains for the sink; at 3.3 V, about 1.296 V remains. This allocation is conditional on the assumed current and forward drop. As current falls, the forward drop changes too. TI's typical curves and particular test conditions do not define a universal minimum headroom for every current and temperature. Our graph deliberately does not draw a fabricated current-versus-voltage knee.

The chip also dissipates power in its logic supply. A planning sum is P_IC = Σ(I_OUT × V_OUT) + VDD × I_DD: add every enabled sink and the logic current. The worked 13.5 mW above describes one sink only. Under the separate illustrative 5 V allocation, two nominal sinks dissipate about 62.4 mW together, plus logic power. The 30 mA limit bounds LED-rail power to at most 150 mW at 5 V, but does not include the separately supplied logic. Do not turn a datasheet thermal-resistance number into a measured breadboard temperature or treat this screen as thermal qualification. Record supply current and ambient conditions; expansion to eight channels needs a fresh assessment.

Complete normal-mode program

The TLC5916 has a shift register and a separate output latch. The supplied driver first requests blanking, shifts all eight bits most-significant first, pulses LE with CLK held LOW, then enables only the connected channels. Bit 0 selects OUT0 and bit 1 selects OUT1. All other bits remain zero. Slow 5 µs software waits separate operations; they are not a measured waveform or a guaranteed complete transaction duration.

OE and LE also participate in special-mode selection. This program keeps OE HIGH during every shift clock and does not clock while the outputs are enabled. It starts from the normal mode and default gain established by a real cold power-up. A soft interpreter restart does not reset an independently powered chip's mode or current configuration. If another program used special mode, switch both rails off before this experiment; this driver does not guess or restore unknown gain settings.

Synthetic normal-mode signals show OE high throughout eight rising shift clocks, LE pulsed only with CLK low and OE low afterwards. Calculated voltage-allocation bars split supply into assumed LED drop, sense drop and remaining output headroom; they are not current measurements.
Original synthetic command timing and conditional voltage-budget calculation. No trace is measured and no regulation knee is predicted. Open full size or download the editable SVG.

Save the full file as tlc5916-current-headroom.py on the Pico and run it through your MicroPython editor's console or read-evaluate-print loop (REPL), the interactive Python prompt. Enter 0, 1, 2, 3 or q. Keep VLED off until the initial blanking request has completed. The program never measures current: its printed commanded_mask is a requested setting. Download the complete program.

"""Normal-mode TLC5916IN headroom lab; original Pico, MicroPython 1.26.0.
Cold power-up required. No special-mode, gain programming or error readout.
Host tests inject pins and delay; host success is not target execution.
"""
import math


def analyse(vled, sense_v, out_v, sense_ohms=10.0, rext_ohms=1800.0):
    """This 1%-resistor fixture only; no regulator model or health verdict."""
    values = (vled, sense_v, out_v, sense_ohms, rext_ohms)
    if any(type(x) not in (int, float) or not math.isfinite(x) for x in values):
        raise ValueError("finite numeric readings and resistances required")
    if not (0 <= vled <= 5 and 0 <= sense_v <= vled and
            0 <= out_v <= vled - sense_v and 9.9 <= sense_ohms <= 10.1 and 1782 <= rext_ohms <= 1818):
        raise ValueError("inconsistent low-voltage fixture readings")
    amps = sense_v / sense_ohms
    if amps > 0.030:
        raise ValueError("reading exceeds the 30 mA fixture limit")
    nominal = 18.75 / rext_ohms
    return {"current_mA": amps * 1000,
            "led_v": vled - sense_v - out_v,
            "headroom_v": out_v,
            "sink_mW": amps * out_v * 1000,
            "nominal_mA": nominal * 1000,
            "current_over_nominal": amps / nominal}


class TLC5916:
    """Commanded output state only; no pin, latch or LED-current readback."""
    def __init__(self, sdi, clk, le, oe, sleep_us):
        self.sdi, self.clk, self.le, self.oe = sdi, clk, le, oe
        self.sleep_us = sleep_us
        self.commanded = None
        self.blanking_requested = None
        self.faulted = False
        self.write(0)

    def close(self):
        """Attempt every cleanup. True means calls succeeded, not measured OFF."""
        ok = True
        self.commanded = None
        self.blanking_requested = None
        for pin, level in ((self.oe, 1), (self.clk, 0), (self.le, 0), (self.sdi, 0)):
            try:
                pin.value(level)
                if pin is self.oe:
                    self.blanking_requested = True
            except BaseException:
                ok = False
        return ok

    def write(self, mask):
        if self.faulted:
            self.close()
            raise RuntimeError("latched software fault: power off VLED and restart the fixture")
        self.commanded = None
        try:
            self.oe.value(1)      # blank before changing any shift/latch signal
            self.blanking_requested = True
            self.le.value(0)
            self.clk.value(0)
            if type(mask) is not int or not 0 <= mask <= 3:
                raise ValueError("only integer masks 0, 1, 2 and 3 are wired")
            self.sleep_us(5)
            for bit in range(7, -1, -1):
                self.sdi.value((mask >> bit) & 1)
                self.sleep_us(5)
                self.clk.value(1)
                self.sleep_us(5)
                self.clk.value(0)
            self.sleep_us(5)
            self.le.value(1)
            self.sleep_us(5)
            self.le.value(0)
            self.sleep_us(5)
            if mask:
                self.oe.value(0)  # no CLK edges while enabled
                self.blanking_requested = False
            self.commanded = mask
            return mask
        except BaseException:
            self.faulted = True
            self.close()
            raise


def make_driver(Pin, sleep_us):
    """OE is constructed first. An init error never deliberately enables it."""
    oe = None
    try:
        oe = Pin(5, Pin.OUT, value=1)
        sdi = Pin(2, Pin.OUT, value=0)
        clk = Pin(3, Pin.OUT, value=0)
        le = Pin(4, Pin.OUT, value=0)
        return TLC5916(sdi, clk, le, oe, sleep_us)
    except BaseException:
        if oe is not None:
            try:
                oe.value(1)
            except BaseException:
                pass
        raise


def main(Pin=None, sleep_us=None, read=input, emit=print):
    if Pin is None:
        from machine import Pin
    if sleep_us is None:
        from time import sleep_us
    driver = None
    try:
        driver = make_driver(Pin, sleep_us)
        emit("Cold power-up only; normal mode and default current gain required.")
        emit("0 blank; 1 OUT0; 2 OUT1; 3 both; q quit. No current is measured by Pico.")
        while True:
            command = read("mask> ").strip()
            if command == "q":
                break
            if command not in ("0", "1", "2", "3"):
                driver.write(0)
                emit("Invalid command; blanking requested. Enter 0, 1, 2, 3 or q.")
                continue
            emit("commanded_mask=" + str(driver.write(int(command))))
    except (Exception, KeyboardInterrupt) as error:
        emit("Stopped: " + str(error) + "; disconnect VLED before inspecting or restarting.")
    finally:
        if driver is not None and not driver.close():
            emit("Cleanup call failed: disconnect VLED; software OFF is unconfirmed.")


if __name__ == "__main__":
    main()

The reusable analyse() function accepts manually measured VLED, sense voltage in volts and OUT voltage. It returns arithmetic quantities without declaring the LED healthy or the current regulated. It accepts only measured sense resistances from 9.9 to 10.1 Ω and R-EXT resistances from 1782 to 1818 Ω, matching this 1% fixture. It rejects non-finite, reversed or inconsistent readings and values outside those bounds rather than clamping them into plausible-looking results. For calculator-only use, save a copy as tlc5916_headroom.py, import it with from tlc5916_headroom import analyse, then call analyse(3.3, 0.1041667, 1.2958333). Importing does not start the interactive hardware program.

What failure and stopping actually mean

An invalid low-level mask blanks the outputs and latches a software fault. A pin-operation or delay exception also requests blanking, attempts every cleanup pin and rejects later writes until the fixture is restarted. The interactive prompt treats a mistyped command more gently: it sends the valid blank mask, reports the mistake and waits for a new valid command. Quitting or interrupting the prompt runs cleanup.

Successful pin calls are not electrical readback. If OE wiring, a GPIO output or its initialization fails, software cannot prove that the physical LEDs are off. Disconnect VLED before touching or restarting a faulted fixture. The external pull-up supports blanking while GPIO is undriven but is not a redundant safety system.

Neither a dim LED nor the driver's faulted flag is a TLC5916 hardware error report. This lesson leaves SDO unconnected and implements no special-mode diagnostic capture. An open LED, inadequate headroom and a wrong command remain different hypotheses to test with the circuit and meter. Do not deliberately short an output or overheat the IC to obtain a fault indication.

Reproduce the checks, then record your experiment

The delivered host tests execute the actual driver with injected pins and delays. They check initialization order, all four masks, bit/latch ordering, invalid input, interruption at every wait position, pin exceptions, a persistent software fault, cleanup attempts, prompt errors and measurement arithmetic. They do not emulate the internal analog regulator, prove GPIO edge timing, compile firmware or exercise a Pico. The independent review and browser stages remain separate until their evidence is recorded.

On hardware, first verify that blanking removes current, then compare the two single-channel sweeps and the both-on sweep. A scope can inspect CLK/SDI/LE/OE timing with its ground at common ground, but it is not needed to invent a regulation claim from appearance. Never attach its ground clip to a high-side sense node. Check actual edge quality and timing against the datasheet if you qualify the interface.

Use the blank observation sheet. It intentionally contains no fabricated results. Record unexpected behaviour and meter/supply limits alongside normal readings. Finally send 0, quit, turn VLED off, then power down the Pico. A larger supply or more connected channels requires a new dissipation and wiring assessment; it is not an automatic extension of this two-channel fixture.

Challenge: is one 10 Ω resistor enough?

Can R2 and R3 be replaced with one 10 Ω resistor feeding both LED anodes, while still using its voltage to compare the two channel currents?

Answer

The common resistor would measure the sum when both channels are enabled. It would no longer reveal the individual currents, and its voltage drop would change with the selected mask, altering both channels' available headroom. Separate sense resistors preserve a separate measurement for each path. They still need tolerance and meter uncertainty recorded.

Primary references and related learning

Continue with the electronics course to compare a resistor-fed LED with this feedback-controlled current path. The same habit—separate the requested setting from the measured result—also helps when testing power supplies, sensor interfaces and motor drivers.