कला
सुन्दरता र कल्याण
हस्तकला
संस्कृति र इतिहास
मनोरञ्जन
वातावरण
खाना र पेय
रिभर्स इन्जिनियरिङ
विज्ञान
खेलकुद
प्रविधि
पहिर्न मिल्ने
Through the Pads: What Your Chip's Pins Do When Nothing Is Connected
Youblob (simulation output) · CC0
Ed

सिर्जनाकर्ता

Ed

29. सेप्टेम्बर 2026FI
१
०
०
०
०

Through the Pads: What Your Chip's Pins Do When Nothing Is Connected

Between your core and the outside world sits a ring of pads: large cells at the edge of the die that the bond wires land on. A signal pad has its own driver, input buffer and switchable pull resistors. wafer.space's template draws that ring in chip_top.sv, and so far this series has tested the core without it. This rung puts the PWM dimmer inside the template's own chip_top, with the PDK's own models of the pad cells, and tests it from outside, pin by pin. Then it unplugs the wires. An input nobody drives floats; in simulation it reads as X, unknown, and every output that depends on it turns unknown too. One line per pad, a pull-down, turns the same unplugged chip into one that sits quietly disabled. Everything here was run for this blueprint with Icarus Verilog 12.0 and cocotb 2.1.0, with and without the pull-downs. The picture is those two runs: the same moment, the same wires coming loose.
उन्नत
An evening

निर्देशनहरू

1

The pad, read off its model

Jupyter नोटबुक लोड हुँदैछ…
2

The core inside the pads

The embedded blueprint writes the dimmer core. This rung wraps it in the template's pad ring and tests it from outside.
3

What chip_top is

chip_top.sv is the whole chip as the bond wires see it. It places one pad cell per pin and connects each to chip_core. In the default build the cells come from the PDK's gf180mcu_fd_io library: in_s, a Schmitt-trigger input, for the clock; in_c, a plain input, for reset and the input pads; bi_24t for each bidirectional pad, controlled from the core by the oe, ie, cs, sl, pu and pd bits the earlier rungs set; asig_5p0 for the analog pads; and dvdd, dvss, vdd and vss cells that only bring in power. A comment in the template notes that with these foundry pads the I/O and core supplies are shorted together, so the whole chip runs from one supply. The analog pads are different. The analog signals are wired straight from the pad to the core's analog port, and the PDK's model of asig_5p0 contains no logic at all: it is only the connection. A digital simulation cannot say anything about what happens on them. chip_top also places a QR code, shuttle and project IDs and a marker, which the template marks as necessary for tapeout, and the wafer.space logo, which it says may be removed.
4

Floating inputs, and what the model can and cannot show

An input pad that nothing drives is not 0. It floats, and whatever it picks up from its surroundings decides what the core reads. In simulation that is z at the pad and X in the core, and X spreads: a comparator with an unknown input gives an unknown output. The fix costs one bit per pad. The bidirectional pad has switchable weak pulls: with pd set, a pad the core is not driving is held low, and anything strong from outside still overrides it. This rung's core sets pd on the eight duty pads and on the input pads, so an unplugged chip reads duty 0 with enable off: it sits dark instead of doing something random. One limit to know: the PDK's model of the plain input pad, in_c, passes the pad straight through and does not model its PU and PD pins. The enable pad's pull-down is set in the design, but this simulation cannot show it working; only the bidirectional pads' pulls are simulated.
5

The core, with pull-downs

Rung 3's chip_core with one change, the pull-downs, behind a PULLDOWNS define so the same file builds both ways. Everything else is the dimmer as before.
chip_core.svsystemverilog
// SPDX-License-Identifier: Apache-2.0
// Youblob silicon rung 6: the rung-3 dimmer, made safe to leave unplugged: every input pad gets a pull-down.
// Port list unchanged from wafer-space/gf180mcu-project-template src/chip_core.sv.

`default_nettype none

module chip_core #(
    parameter NUM_INPUT_PADS,
    parameter NUM_BIDIR_PADS,
    parameter NUM_ANALOG_PADS
    )(
    `ifdef USE_POWER_PINS
    inout  wire VDD,
    inout  wire VSS,
    `endif

    input  wire clk,       // clock
    input  wire rst_n,     // reset (active low)

    input  wire [NUM_INPUT_PADS-1:0] input_in,   // Input value
    output wire [NUM_INPUT_PADS-1:0] input_pu,   // Pull-up
    output wire [NUM_INPUT_PADS-1:0] input_pd,   // Pull-down

    input  wire [NUM_BIDIR_PADS-1:0] bidir_in,   // Input value
    output wire [NUM_BIDIR_PADS-1:0] bidir_out,  // Output value
    output wire [NUM_BIDIR_PADS-1:0] bidir_oe,   // Output enable
    output wire [NUM_BIDIR_PADS-1:0] bidir_cs,   // Input type (0=CMOS Buffer, 1=Schmitt Trigger)
    output wire [NUM_BIDIR_PADS-1:0] bidir_sl,   // Slew rate (0=fast, 1=slow)
    output wire [NUM_BIDIR_PADS-1:0] bidir_ie,   // Input enable
    output wire [NUM_BIDIR_PADS-1:0] bidir_pu,   // Pull-up
    output wire [NUM_BIDIR_PADS-1:0] bidir_pd,   // Pull-down

    inout  wire [NUM_ANALOG_PADS-1:0] analog  // Analog
);

    // Pad 0..7: the duty value comes IN.  Pad 8: the PWM goes OUT.  The rest: outputs held low.
    localparam DUTY_W = 8;
    localparam PWM_PAD = 8;

    // An input nobody drives floats: it reads as noise, and so does everything that depends on it.
    // With PULLDOWNS defined, every input pad is pulled low, so an unplugged chip sits disabled at duty 0.
    // (Rung 3 left them all off; build without PULLDOWNS to see what that does.)
    assign input_pu = '0;
`ifdef PULLDOWNS
    assign input_pd = '1;
`else
    assign input_pd = '0;
`endif

    // Output enable only on pads PWM_PAD and above; inputs get their input buffer enabled.
    assign bidir_oe = {{(NUM_BIDIR_PADS-DUTY_W){1'b1}}, {DUTY_W{1'b0}}};
    assign bidir_ie = ~bidir_oe;
    assign bidir_cs = {{(NUM_BIDIR_PADS-DUTY_W){1'b0}}, {DUTY_W{1'b1}}};  // Schmitt trigger on the inputs
    assign bidir_sl = '0;
    assign bidir_pu = '0;
`ifdef PULLDOWNS
    assign bidir_pd = {{(NUM_BIDIR_PADS-DUTY_W){1'b0}}, {DUTY_W{1'b1}}};   // the pad ignores PD while it drives
`else
    assign bidir_pd = '0;
`endif

    wire enable = input_in[0];
    wire [DUTY_W-1:0] duty = bidir_in[DUTY_W-1:0];

    logic [DUTY_W-1:0] count;
    always_ff @(posedge clk) begin
        if (!rst_n)
            count <= '0;
        else if (enable)
            count <= count + 1'b1;
    end

    // High while the free-running count is below the duty value: duty/256 of every period.
    logic pwm;
    always_ff @(posedge clk) begin
        if (!rst_n)
            pwm <= 1'b0;
        else
            pwm <= enable && (count < duty);
    end

    logic [NUM_BIDIR_PADS-1:0] out;
    always_comb begin
        out = '0;
        out[PWM_PAD] = pwm;
    end
    assign bidir_out = out;

    // Inputs this design does not read (keeps lint quiet, as the template does with _unused).
    logic _unused;
    assign _unused = &{1'b0, input_in[NUM_INPUT_PADS-1:1], bidir_in[NUM_BIDIR_PADS-1:DUTY_W]};

endmodule

`default_nettype wire

चाहिने औजार:

टेबुल कम्प्युटरटेबुल कम्प्युटर
6

The outside world

tb_pads.sv wraps the template's unchanged chip_top. Each pad is either driven by the testbench or released to z, one bit at a time, the way a breakout with some wires unplugged would leave it.
tb_pads.svsystemverilog
// SPDX-License-Identifier: Apache-2.0
// The outside world around chip_top: every pad is either driven by the testbench or left floating (z),
// one bit at a time, the way a breakout board with some wires unplugged would leave it.
`default_nettype none
`include "slot_defines.svh"
module tb_pads;
    reg clk = 0, rst_n = 0;
    reg  [`NUM_INPUT_PADS-1:0] in_val = 0, in_drive = 0;
    reg  [`NUM_BIDIR_PADS-1:0] bi_val = 0, bi_drive = 0;

    wire clk_PAD = clk;
    wire rst_n_PAD = rst_n;
    wire [`NUM_INPUT_PADS-1:0] input_PAD;
    wire [`NUM_BIDIR_PADS-1:0] bidir_PAD;
    wire [`NUM_ANALOG_PADS-1:0] analog_PAD;

    genvar i;
    for (i = 0; i < `NUM_INPUT_PADS; i = i + 1) assign input_PAD[i] = in_drive[i] ? in_val[i] : 1'bz;
    for (i = 0; i < `NUM_BIDIR_PADS; i = i + 1) assign bidir_PAD[i] = bi_drive[i] ? bi_val[i] : 1'bz;

    chip_top chip (.clk_PAD(clk_PAD), .rst_n_PAD(rst_n_PAD), .input_PAD(input_PAD),
                   .bidir_PAD(bidir_PAD), .analog_PAD(analog_PAD));

    wire pwm_pad = bidir_PAD[8];
endmodule
`default_nettype wire

चाहिने औजार:

टेबुल कम्प्युटरटेबुल कम्प्युटर
7

The tests

test_pads.py builds chip_top, this core, the PDK's pad models (gf180mcu_fd_io.v) and the template's five ID and logo models, at the template's 25 MHz. Three tests: rung 3's duty test, now through the pads; the duty wires coming loose; and something outside driving the chip's output pad. Run it twice, with PULLDOWNS=1 and without.
test_pads.pypython
# SPDX-License-Identifier: Apache-2.0
# The dimmer tested from OUTSIDE the chip: through the template's chip_top and the PDK's own I/O pad models
# (gf180mcu_fd_io, gf180mcuD, the commit the template pins). Run twice: with and without the pull-downs.
import os
from pathlib import Path

import cocotb
from cocotb.clock import Clock
from cocotb.triggers import ClockCycles, FallingEdge
from cocotb_tools.runner import get_runner

PULLDOWNS = bool(os.getenv("PULLDOWNS"))
HERE = Path(__file__).resolve().parent
PDK = Path(os.getenv("PDK_ROOT", HERE / "pdk")) / "gf180mcuD" / "libs.ref"
TEMPLATE = Path(os.getenv("TEMPLATE", HERE / "template"))


async def power_up(dut):
    cocotb.start_soon(Clock(dut.clk, 40, unit="ns").start())      # 25 MHz, the template's clock constraint
    dut.rst_n.value = 0
    await ClockCycles(dut.clk, 5)
    dut.rst_n.value = 1


def drive_duty(dut, duty, enable=1):
    dut.bi_val.value = duty
    dut.bi_drive.value = 0xFF          # the testbench drives pads 0..7 only; pad 8 is the chip's
    dut.in_val.value = enable
    dut.in_drive.value = 1             # input pad 0 = enable


async def pad_highs(dut, clocks):
    """Count the high clocks on bidir pad 8, and the clocks where it is neither 0 nor 1."""
    high = unknown = 0
    for _ in range(clocks):
        await FallingEdge(dut.clk)
        v = dut.pwm_pad.value
        if not v.is_resolvable:
            unknown += 1
        elif int(v):
            high += 1
    return high, unknown


@cocotb.test()
async def test_duty_through_the_pads(dut):
    """Rung 3's test again, but every signal goes through a real pad model on its way in and out."""
    await power_up(dut)
    for duty in (0, 1, 64, 128, 200, 255):
        drive_duty(dut, duty)
        await ClockCycles(dut.clk, 256)
        high, unknown = await pad_highs(dut, 1024)
        dut._log.info(f"duty {duty:3d}: pad 8 high {high} of 1024 clocks, unknown {unknown}")
        assert unknown == 0 and high == 4 * duty


@cocotb.test()
async def test_duty_pads_unplugged(dut):
    """The eight duty wires come loose. With pull-downs the chip reads 0 and the output stays low;
    without them the core reads X, and the output pad goes unknown."""
    await power_up(dut)
    drive_duty(dut, 200)
    await ClockCycles(dut.clk, 512)
    trace = []
    for _ in range(512):                       # the last 512 clocks while plugged in...
        await FallingEdge(dut.clk)
        trace.append(str(dut.pwm_pad.value))
    dut.bi_drive.value = 0             # the duty pads float
    for _ in range(512):                       # ...and the first 512 after the wires come loose
        await FallingEdge(dut.clk)
        trace.append(str(dut.pwm_pad.value))
    (HERE / f"trace_{'pd' if PULLDOWNS else 'nopd'}.csv").write_text("\n".join(trace))
    high, unknown = await pad_highs(dut, 1024)
    y = dut.chip.bidir_PAD2CORE.value
    dut._log.info(f"pull-downs {'ON ' if PULLDOWNS else 'OFF'}: core sees duty pads as "
                  f"{str(y)[-8:]}; pad 8 high {high}, unknown {unknown} of 1024 clocks")
    if PULLDOWNS:
        assert unknown == 0 and high == 0
    else:
        assert unknown > 0


@cocotb.test()
async def test_output_pad_fight(dut):
    """Something outside drives pad 8 while the chip drives it too: the pad value is unknown.
    In silicon that is two drivers shorting each other."""
    await power_up(dut)
    drive_duty(dut, 128)
    await ClockCycles(dut.clk, 300)
    dut.bi_val.value = 128                          # keep the duty
    dut.bi_drive.value = 0xFF | (1 << 8)            # ...and now also drive pad 8 low from outside
    high, unknown = await pad_highs(dut, 256)
    dut._log.info(f"pad 8 driven from both sides: unknown for {unknown} of 256 clocks")
    assert unknown > 0


def test_runner():
    defines = {"SLOT_0P5X0P5": True, "PDK_gf180mcuD": True, "SCL_gf180mcu_fd_sc_mcu7t5v0": True,
               "PAD_gf180mcu_fd_io": True, "SRAM_gf180mcu_fd_ip_sram": True}
    if PULLDOWNS:
        defines["PULLDOWNS"] = True
    ip = TEMPLATE / "ip"
    sources = [HERE / "chip_top.sv", HERE / "chip_core.sv", HERE / "tb_pads.sv",
               PDK / "gf180mcu_fd_io" / "verilog" / "gf180mcu_fd_io.v"] + \
              [ip / n / "vh" / f"{n}.v" for n in ("gf180mcu_ws_ip__logo", "gf180mcu_ws_ip__marker",
               "gf180mcu_ws_ip__qrcode_id", "gf180mcu_ws_ip__shuttle_id", "gf180mcu_ws_ip__project_id")]
    runner = get_runner("icarus")
    build = f"sim_build_{'pd' if PULLDOWNS else 'nopd'}"
    runner.build(sources=sources, hdl_toplevel="tb_pads", defines=defines, includes=[HERE],
                 build_args=["-g2012"], timescale=("1ns", "1ps"), always=True, build_dir=build)
    runner.test(hdl_toplevel="tb_pads", test_module="test_pads", build_dir=build)


if __name__ == "__main__":
    test_runner()

चाहिने औजार:

टेबुल कम्प्युटरटेबुल कम्प्युटर
8

What the simulation printed

Both builds, Icarus Verilog 12.0 and cocotb 2.1.0: Without pull-downs (rung 3's core): duty 0..255 through the pads: high 0, 4, 256, 512, 800, 1020 of 1024 clocks, unknown 0 duty wires unplugged: core sees duty pads as XXXXXXXX; pad 8 high 0, unknown 1024 of 1024 clocks pad 8 driven from both sides: unknown for 128 of 256 clocks TESTS=3 PASS=3 FAIL=0 SKIP=0 With pull-downs (PULLDOWNS defined): duty 0..255 through the pads: the same six counts, unknown 0 duty wires unplugged: core sees duty pads as 00000000; pad 8 high 0, unknown 0 of 1024 clocks pad 8 driven from both sides: unknown for 128 of 256 clocks TESTS=3 PASS=3 FAIL=0 SKIP=0 The unplugged test asserts opposite things in the two builds, so each is the other's proof that the test can fail: without pull-downs it demands unknown output, with them it demands none. The fight on pad 8 is unknown for exactly half the clocks at duty 128: while the chip drives low and the outside also drives low they agree; while the chip drives high they fight. In silicon that is two drivers shorting the supply through each other. Never drive a pin the chip is driving.
9

Pins that read wrong from outside

Pad-level troubleshooting.

Flow

Loading...
10

Sources and honest limits

**Sources**, read 29 September 2026: the wafer-space/gf180mcu-project-template repository (src/chip_top.sv, unchanged; src/slot_defines.svh; the five ip/*/vh models; cocotb/chip_top_tb.py for the source list; Apache-2.0). The GF180MCU PDK at the commit the template pins (gf180mcuD, f6eeac7dad085ffcc829ccfd721f7b4ce39edcf7, from the fossi-foundation ciel releases; Apache-2.0): gf180mcu_fd_io.v, the pad models. The run-1 pad table in wafer-space/chip-on-board-wire-bonded-pcbs for the 74-pad convention. **Honest limits.** The pad models are logic only: no pull resistor values, drive strengths, thresholds or timing, and the input pad's pulls are not modelled at all. The generated_defines.svh file the template's Makefile writes was written by hand for this run. Nothing here has been made.

आवश्यक उपकरणहरू

1

CC0 सार्वजनिक डोमेन

यो ब्लुप्रिन्ट CC0 अन्तर्गत जारी गरिएको छ। तपाईं अनुमति नसोधी प्रतिलिपि, परिमार्जन, वितरण र प्रयोग गर्न सक्नुहुन्छ।

ब्लुप्रिन्ट मार्फत उत्पादनहरू किनेर सिर्जनाकर्तालाई सहयोग गर्नुहोस् सिर्जनाकर्ता कमिसन विक्रेताले तोकेको, वा यो ब्लुप्रिन्टको नयाँ संस्करण बनाउनुहोस् र आम्दानी बाँड्न आफ्नो ब्लुप्रिन्टमा जडानको रूपमा समावेश गर्नुहोस्।

छलफल

(0)

लग इन छलफलमा सामेल हुन

टिप्पणीहरू लोड गर्दै...