협업
예술
뷰티 및 웰니스
공예
문화 및 역사
엔터테인먼트
환경
음식 및 음료
역공학
과학
스포츠
기술
웨어러블
온칩 SRAM: 칩 자체 메모리에 저장된 호흡하는 빛
English
Youblob (simulation output) · CC0
Ed

작성자

Ed

29. 9월 2026FI
49
0
0
0
0

온칩 SRAM: 칩 자체 메모리에 저장된 호흡하는 빛

wafer.space 템플릿의 예제 설계에는 각 512바이트 크기의 SRAM 매크로 두 개가 들어 있으며, 두 매크로는 꺼진 상태로 남겨져 있습니다. 매크로는 PDK에서 가져온 완성된 메모리 블록으로, 4,096개의 정적 메모리 셀을 주소 디코더와 읽기 회로와 함께 한 번에 설계한 것이며, 게이트부터 직접 만들지 않고 통째로 배치할 수 있습니다. 이번 단계에서는 이 매크로 중 하나를 켭니다. 이전 단계의 PWM 디머는 이제 패턴을 재생합니다. 마이크로컨트롤러가 패드를 통해 256바이트를 칩에 한 번 기록하면, 이후 칩은 스스로 한 번에 한 바이트씩 값을 읽어 천천히 호흡하는 빛의 밝기로 출력합니다. 이 블루프린트를 위해 코어와 테스트벤치는 Icarus Verilog 12.0, cocotb 2.1.0, 그리고 PDK의 SRAM 매크로 자체 시뮬레이션 모델을 사용해 실행되었습니다. 출력은 저장된 테이블을 600개의 PWM 주기 동안 따랐습니다. 그림은 주기마다 막대 하나씩으로 나타낸 이 시뮬레이션의 실제 출력입니다. 테이블을 로드하는 ESP32 스케치는 이 블루프린트에 맞게 컴파일되었으며, 실행할 실제 칩은 아직 없습니다.
고급
저녁, 그리고 칩

안내

1

패턴, 타이밍, 그리고 크기

Jupyter 노트북 불러오는 중…
2

4,096개 비트 각각이 무엇인지

내장된 블루프린트는 정적 메모리의 한 비트가 어떻게 스스로를 유지하는지 보여줍니다. 두 개의 인버터로 이루어진 루프입니다. 이 단계의 매크로는 하나의 어레이에 있는 4,096비트의 정적 메모리입니다.
3

매크로의 핀과 모델이 말하는 세 가지

gf180mcu_fd_ip_sram__sram512x8m8wm1은 512바이트를 담습니다. 핀은 CLK; A, 9비트 주소; D, 기록할 바이트; Q, 읽은 바이트입니다. 세 가지 제어 신호는 모두 active LOW입니다. CEN가 0이면 메모리가 켜집니다. GWEN가 0이면 클록 에지는 기록이 되고, 1이면 읽기가 됩니다. WEN는 데이터 비트마다 하나씩 있으며, 0이면 해당 비트를 기록할 수 있어서, 기록 시 한 바이트의 일부 비트만 바꾸고 나머지는 그대로 둘 수 있습니다.
4

The core

src/chip_core.sv for the pattern player, with the template's port list unchanged plus one parameter, HOLD_LOG2. Input pad 0 enables, pad 1 selects LOAD mode, pad 2 is the write strobe; bidirectional pads 0 to 7 carry the byte, pad 8 the PWM. The LOAD and strobe pads are passed through two flip-flops before use, a synchroniser: they change whenever the outside world likes, not in step with the chip's clock, and a flip-flop caught mid-change can hover between 0 and 1 for a while. In play mode, each time HOLD_LOG2 periods have passed, the byte the SRAM has been reading becomes the new duty and the address moves on.
chip_core.svsystemverilog
// SPDX-License-Identifier: Apache-2.0
// Youblob silicon rung 5: the PWM dimmer plays a brightness pattern stored in the on-chip SRAM macro.
// 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,
    parameter HOLD_LOG2 = 9        // each table entry lasts 2**HOLD_LOG2 PWM periods
    )(
    `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
);

    // Input pad 0: enable.  Input pad 1: LOAD mode.  Input pad 2: write strobe.
    // Bidir pads 0..7: the byte to store while loading.  Bidir pad 8: the PWM output.
    localparam DUTY_W = 8;
    localparam PWM_PAD = 8;

    assign input_pu = '0;
    assign input_pd = '0;
    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}}};
    assign bidir_sl = '0;
    assign bidir_pu = '0;
    assign bidir_pd = '0;

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

    // The mode and strobe pads change whenever the outside world likes, not in step with clk:
    // two flip-flops in a row (a synchroniser) before anything uses them.
    logic [1:0] load_s;
    logic [2:0] strobe_s;
    always_ff @(posedge clk) begin
        load_s   <= {load_s[0], input_in[1]};
        strobe_s <= {strobe_s[1:0], input_in[2]};
    end
    logic load_was;
    wire load        = load_s[1];
    wire load_start  = load & ~load_was;
    wire strobe_rise = strobe_s[1] & ~strobe_s[2];
    always_ff @(posedge clk) load_was <= load;

    // The SRAM wants its chip enable (CEN, active LOW) to go 1 -> 0 before it works:
    // hold it at 1 through reset, then 0.
    logic cen;
    always_ff @(posedge clk) cen <= !rst_n;

    // PWM counter and the "hold" counter that decides when to step to the next table entry.
    logic [DUTY_W-1:0] count;
    logic [HOLD_LOG2:0] hold;                   // one bit wider so HOLD_LOG2 = 0 still works
    wire period_end = (count == '1);
    localparam logic [HOLD_LOG2:0] HOLD_MASK = (1 << HOLD_LOG2) - 1;   // 0 when HOLD_LOG2 = 0
    wire step = period_end && ((hold & HOLD_MASK) == HOLD_MASK);

    logic [DUTY_W-1:0] addr;
    logic [DUTY_W-1:0] duty;
    wire  [DUTY_W-1:0] q;                       // SRAM read data: mem[addr], one clock after addr

    always_ff @(posedge clk) begin
        if (!rst_n) begin
            count <= '0;  hold <= '0;  addr <= '0;  duty <= '0;
        end else if (load) begin
            count <= '0;  hold <= '0;  duty <= '0;
            if (load_start)       addr <= '0;       // every load starts at entry 0
            else if (strobe_rise) addr <= addr + 1'b1;
        end else if (enable) begin
            count <= count + 1'b1;
            if (period_end) hold <= hold + 1'b1;
            if (step) begin
                duty <= q;                          // the entry that was being read
                addr <= addr + 1'b1;                // and move on to the next
            end
        end
    end

    `gf180mcu_xxx_ip_sram__sram512x8m8wm1 sram_0 (
        `ifdef USE_POWER_PINS
        .VDD  (VDD),
        .VSS  (VSS),
        `endif
        .CLK  (clk),
        .CEN  (cen),                                   // active low: 0 = on
        .GWEN (~(load && strobe_rise)),                // active low: 0 = write this clock
        .WEN  (8'b0),                                  // active low, per bit: write all eight
        .A    ({1'b0, addr}),                          // 256 of the 512 bytes
        .D    (data_in),
        .Q    (q)
    );

    logic pwm;
    always_ff @(posedge clk) begin
        if (!rst_n) pwm <= 1'b0;
        else        pwm <= enable && !load && (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:3], bidir_in[NUM_BIDIR_PADS-1:DUTY_W], hold[HOLD_LOG2]};

endmodule

`default_nettype wire

필요한 도구:

데스크톱 컴퓨터데스크톱 컴퓨터
5

The test

test_pattern.py builds the core with the PDK's SRAM model, gf180mcu_fd_ip_sram__sram512x8m8wm1.v, at the template's 25 MHz and with HOLD_LOG2 = 0 so the whole table plays in 256 periods. It loads the table through the pads the way the ESP32 will, then counts the high clocks in each of 600 PWM periods and requires them to follow the table from exactly one starting point. A second test loads again and checks the new pattern replaced the old.
test_pattern.pypython
# SPDX-License-Identifier: Apache-2.0
# cocotb testbench for the SRAM pattern player, with the PDK's own SRAM simulation model
# (gf180mcu_fd_ip_sram__sram512x8m8wm1, gf180mcuD, the commit the template pins).
import math
import os
from pathlib import Path

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

PWM_PAD = 8
EN, LOAD, STROBE = 1, 2, 4                      # input pad bits
SRAM = Path(os.getenv("PDK_ROOT", "pdk")) / "gf180mcuD" / "libs.ref" / "gf180mcu_fd_ip_sram" / "verilog"


def breathing_table(n=256, gamma=2.2):
    """One slow breath: a raised sine, gamma-corrected so equal steps LOOK equal to the eye."""
    return [round(255 * ((1 - math.cos(2 * math.pi * i / n)) / 2) ** gamma) for i in range(n)]


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


async def load(dut, table):
    """What a microcontroller on the breakout does: LOAD high, then per byte set the data pads,
    raise the strobe, wait, lower it. The chip synchronises the strobe, so hold each for 4 clocks."""
    dut.input_in.value = LOAD
    await ClockCycles(dut.clk, 4)
    for b in table:
        dut.bidir_in.value = b
        await ClockCycles(dut.clk, 1)
        dut.input_in.value = LOAD | STROBE
        await ClockCycles(dut.clk, 4)
        dut.input_in.value = LOAD
        await ClockCycles(dut.clk, 4)
    dut.input_in.value = 0
    await ClockCycles(dut.clk, 4)


async def highs_per_period(dut, periods):
    """High clocks in each PWM period, counted from the core's own count = 0."""
    while int(dut.count.value) != 0:
        await RisingEdge(dut.clk)
    out = []
    for _ in range(periods):
        n = 0
        for _ in range(256):
            await FallingEdge(dut.clk)
            n += int(dut.bidir_out.value[PWM_PAD])
        out.append(n)
    return out


@cocotb.test()
async def test_plays_the_table(dut):
    """Load a 256-entry breathing table, play it: each period's high time is the next entry."""
    table = breathing_table()
    await start(dut)
    await load(dut, table)
    dut.input_in.value = EN
    got = await highs_per_period(dut, 600)
    # the output starts part-way through the table; find where, then demand every period match
    fits = [off for off in range(256) if all(got[j] == table[(j + off) % 256] for j in range(2, 600))]
    dut._log.info(f"first periods {got[:8]}, table aligns at offset {fits}")
    Path(__file__).resolve().parent.joinpath("pattern_trace.csv").write_text("\n".join(map(str, got)))
    assert len(fits) == 1, f"the output does not follow the table (offsets that fit: {fits})"


@cocotb.test()
async def test_reload(dut):
    """Loading again replaces the pattern: a flat 64 gives 64 high clocks every period."""
    await start(dut)
    await load(dut, breathing_table())
    await load(dut, [64] * 256)
    dut.input_in.value = EN
    got = await highs_per_period(dut, 40)
    assert all(g == 64 for g in got[2:]), got


def test_runner():
    here = Path(__file__).resolve().parent
    defines = {"gf180mcu_xxx_ip_sram__sram512x8m8wm1": "gf180mcu_fd_ip_sram__sram512x8m8wm1"}
    runner = get_runner("icarus")
    runner.build(
        sources=[SRAM / "gf180mcu_fd_ip_sram__sram512x8m8wm1.v", here / "chip_core.sv"],
        hdl_toplevel="chip_core",
        parameters={"NUM_INPUT_PADS": 4, "NUM_BIDIR_PADS": 38, "NUM_ANALOG_PADS": 4, "HOLD_LOG2": 0},
        defines=defines,
        build_args=["-g2012"],
        timescale=("1ns", "1ps"),
        always=True,
    )
    runner.test(hdl_toplevel="chip_core", test_module="test_pattern")


if __name__ == "__main__":
    test_runner()

필요한 도구:

데스크톱 컴퓨터데스크톱 컴퓨터
6

시뮬레이션이 출력한 내용

이 블루프린트를 Icarus Verilog 12.0 및 cocotb 2.1.0으로 실행한 결과: -------- MESSAGE: CEN 방금 리셋되었고, 메모리는 정상 작동 중 --------- test_pattern.test_plays_the_table PASS (600 주기, 테이블이 정확히 하나의 오프셋에서 정렬됨) test_pattern.test_reload PASS TESTS=2 PASS=2 FAIL=0 SKIP=0 테스트가 실패할 수 있음을 증명하기 위해, WEN을 8'b0에서 8'hFF로 변경했습니다. 이는 active-low 핀에서 흔히 발생하는 실수로, 모든 비트가 마스킹되어 아무것도 기록되지 않고 0으로 구성된 테이블이 만들어집니다. 두 테스트 모두 실패했고, 코어는 원래대로 되돌려졌습니다. 첫 번째 줄은 SRAM 모델 자체의 메시지입니다: 모델이 요구하는 대로, 코어는 리셋 후 CEN을 낮췄습니다.
7

Load it from an ESP32

Once the chip is on its breakout board, this sketch writes the table. Eight GPIOs carry the byte, three more drive LOAD, the strobe and enable, and an LED with a 330 ohm resistor from pad 8 to ground shows the breath. Each level is held for 1 microsecond, 25 of the chip's clocks, so the synchroniser always sees it. The sketch then does nothing: the pattern lives in the chip. It is lost when the chip loses power, so send r over serial to load it again. The ESP32 is a 3.3 V part and its inputs are not 5 V tolerant. The sketch only drives the chip and never reads from it. The template's default libraries are the 5 V ones; before you connect, check in the I/O library's documentation that 3.3 V is a valid logic high for your chip's inputs, or build with the 3.3 V libraries, as the template's README shows. The sketch compiles for the ESP32 with the Arduino ESP32 core 3.3.12; it has not been run, because the chip does not exist yet.
load_pattern.inocpp
// SPDX-License-Identifier: Apache-2.0
// Load a breathing pattern into your chip's SRAM through its pads, then let the chip play it.
// Youblob silicon rung 5. The chip does the playing; this board only writes 256 bytes once.
//
// Wiring (ESP32 GPIO -> chip pad on your breakout):
//   D0..D7  GPIO 13,14,18,19,21,22,23,25 -> bidir[0..7]
//   LOAD    GPIO 26 -> input[1]
//   STROBE  GPIO 27 -> input[2]
//   EN      GPIO 32 -> input[0]
//   and a common ground. The chip's bidir[8] drives an LED through 330 ohm to ground.
// The ESP32 is a 3.3 V part: run the chip's I/O at 3.3 V, or put a level shifter between them.

#include <math.h>

const int DATA_PINS[8] = {13, 14, 18, 19, 21, 22, 23, 25};
const int PIN_LOAD = 26, PIN_STROBE = 27, PIN_EN = 32;

// The chip passes LOAD and STROBE through a two-flip-flop synchroniser and looks for the strobe's
// rising edge, so every level has to last a few of ITS clocks. 1 us is 25 clocks at 25 MHz.
const unsigned HOLD_US = 1;

uint8_t table[256];

void makeBreathingTable() {
  // a raised sine, gamma-corrected (2.2) so the brightness steps look even to the eye
  for (int i = 0; i < 256; i++) {
    float s = (1.0f - cosf(2.0f * PI * i / 256.0f)) / 2.0f;
    table[i] = (uint8_t)lroundf(255.0f * powf(s, 2.2f));
  }
}

void putByte(uint8_t b) {
  for (int bit = 0; bit < 8; bit++) digitalWrite(DATA_PINS[bit], (b >> bit) & 1);
  delayMicroseconds(HOLD_US);            // data settles before the strobe
  digitalWrite(PIN_STROBE, HIGH);        // the chip writes on the strobe's rising edge...
  delayMicroseconds(HOLD_US);            // ...a few of its clocks later, so hold the data
  digitalWrite(PIN_STROBE, LOW);
  delayMicroseconds(HOLD_US);
}

void loadTable() {
  digitalWrite(PIN_EN, LOW);
  digitalWrite(PIN_LOAD, HIGH);          // entering LOAD resets the chip's address to 0
  delayMicroseconds(HOLD_US);
  for (int i = 0; i < 256; i++) putByte(table[i]);
  digitalWrite(PIN_LOAD, LOW);
  delayMicroseconds(HOLD_US);
}

void setup() {
  Serial.begin(115200);
  for (int bit = 0; bit < 8; bit++) pinMode(DATA_PINS[bit], OUTPUT);
  pinMode(PIN_LOAD, OUTPUT);
  pinMode(PIN_STROBE, OUTPUT);
  pinMode(PIN_EN, OUTPUT);
  digitalWrite(PIN_STROBE, LOW);

  makeBreathingTable();
  loadTable();
  digitalWrite(PIN_EN, HIGH);            // play
  Serial.println("pattern loaded; the chip is playing it on its own");
  Serial.printf("table[0]=%u table[64]=%u table[128]=%u\n", table[0], table[64], table[128]);
}

void loop() {
  // Nothing to do: the pattern lives in the chip's SRAM now.
  // Send 'r' over serial to load it again (the SRAM loses it when the chip loses power).
  if (Serial.available() && Serial.read() == 'r') {
    loadTable();
    digitalWrite(PIN_EN, HIGH);
    Serial.println("reloaded");
  }
}

이 단계의 재료:

맞춤형 ASIC 다이맞춤형 ASIC 다이1 개
인쇄 회로 기판인쇄 회로 기판1 개
5 mm LED5 mm LED1 개
330 옴 저항330 옴 저항1 개

필요한 도구:

ESP32 개발 보드ESP32 개발 보드
브레드보드브레드보드
점퍼 와이어점퍼 와이어
데스크톱 컴퓨터데스크톱 컴퓨터
8

저장되지 않거나 재생되지 않는 패턴

SRAM 패턴 문제 해결.

Flow

Loading...
9

출처와 정직한 한계

**출처**, 2026년 9월 29일 확인: 템플릿이 고정한 커밋(gf180mcuD, f6eeac7dad085ffcc829ccfd721f7b4ce39edcf7, fossi-foundation ciel 릴리스 기준; Apache-2.0)의 GF180MCU PDK: SRAM 모델 gf180mcu_fd_ip_sram__sram512x8m8wm1.v, 외곽선을 위한 LEF, 최소 클럭 주기를 위한 타이밍 파일; 플립플롭 면적을 위한 표준 셀 라이브러리의 타이밍 파일. wafer-space/gf180mcu-project-template 저장소(src/chip_core.sv, src/chip_top.sv, README.md, librelane/config.yaml; Apache-2.0). **정직한 한계.** 시뮬레이션은 chip_core 수준에서 이루어졌으며, SRAM의 자체 모델을 사용하지만 패드 링, 합성, 레이아웃은 포함하지 않습니다. 모델의 타이밍 검사는 여기서 아무것도 결정하지 않습니다. ESP32 스케치는 컴파일되었을 뿐 실행되지는 않았습니다. HOLD_LOG2은 테스트에서 0이었으므로 종료될 수 있도록 했으며, 칩에서는 9를 사용하게 되며, 약 1.3초의 숨 고르기 시간입니다.

재료

4
  • 플레이스홀더
  • 플레이스홀더
  • 1 개
    1 vendor sell this, none ship to you yet
    플레이스홀더
  • 플레이스홀더

필요 도구

4

CC0 퍼블릭 도메인

이 블루프린트는 CC0로 공개되었습니다. 어떤 목적으로든 자유롭게 복사, 수정, 배포 및 사용할 수 있습니다.

제품 구매를 통해 메이커를 지원하세요. 판매자가 설정한 메이커 커미션 을 받거나, 이 블루프린트의 새로운 반복을 만들어 연결로 포함시킬 수 있습니다.

토론

(0)

로그인 하여 토론에 참여하세요

댓글 로딩 중...