
A digital spiking-neuron network for the IHP SG13G2 shuttle (TTIHP-26b). The active source set emulates Adaptive Exponential (AdEx) integrate-and-fire dynamics with DPI-style synaptic coupling using shifts, adds and subtracts only: project.v, adex_config.v, adex_block.v, adex_pair.v, and adex_network.v contain no multiplier, divider, or lookup table.
The baseline configuration is a population of four blocks forming two excitatory/inhibitory (E/I) pairs with reciprocal inhibition. Each block is a self-contained population primitive: 1 membrane prime, 2 fast-positive units, 3 slow-negative adaptation units, and 3 small slow-period counters. The architecture is compositional: adex_block -> adex_pair -> adex_network.
All state variables are signed fixed-point. The membrane prime v is Q4.12 (1.0 = 4096), fast units are 10-bit signed, slow units are 12-bit signed. Every term is a shift-scaled constant or a state value; coupling is a binary selection of a constant, never a product.
Prime (membrane) update, no spike:
v' = v - (v >> KV) + fast_drive - slow_drive + Iext - inh + exc
Fast-positive units (drive the upstroke; accumulate while v > VTRIG):
f_i' = f_i - (f_i >> KF_i) + (v > VTRIG ? FINC_i : 0)
Slow-negative units use a 7-bit phase counter $p_i$ and their configured period $KS_i$:
p_i' = (p_i = KS_i - 1) ? 0 : p_i + 1
On a period tick, the unit relaxes toward zero by one eighth of its magnitude, with a minimum one-count change to avoid a quantisation dead zone. Otherwise it holds its value. Every slow unit receives the shared runtime WBUMP_Q on the block's own spike:
w_i' = w_i + (p_i = KS_i - 1 ? relax(w_i) : 0) + (v > VTH ? WBUMP_Q : 0)
On v > VTH the block emits a registered spike and performs a subtractive reset v' = v - VSTEP (classic AdEx spike-and-reset, without the exponential term: the fast units supply the upstroke instead). Drive contributions are
fast_drive = (f0 >> FSH0) + (f1 >> FSH1)
slow_drive = (w0 >> SSH0) + (w1 >> SSH1) + (w2 >> SSH2)
all state updates are saturated to their storage widths. This is the "shift-only AdEx emulation" described in src/implementation_plan.md; the synthesised RTL is definitive for fixed-point and shift semantics.
Each block carries three slow-negative units with distinct integer update periods. KS_i is the number of core cycles between relaxation updates, not a right-shift exponent. With the fixed one-eighth relaxation, the approximate exponential time constant is $8KS_i$ cycles at magnitudes above eight counts; the minimum one-count relaxation completes the return to zero at low magnitudes.
| Pair | E slow periods in cycles (KS0, KS1, KS2) | I slow periods in cycles (KS0, KS1, KS2) |
|---|---|---|
| pair 0 | (5, 7, 11) | (13, 17, 19) |
| pair 1 | (23, 29, 31) | (37, 41, 43) |
| pair 2 (stretch, N_PAIRS=3) | (47, 53, 59) | (61, 67, 71) |
All three slow units bump on the block's own spike (W += WBUMP_Q each). This produces spike-frequency adaptation: the slow units accumulate during a spike train and relax at their independently scheduled periods.
adex_block — the population primitive above (parameters in the next section).adex_pair — one E block and one I block with reciprocal inhibition: E spikes inhibit I and vice versa (inh_in port, magnitude = runtime INH_AMT_Q, default 512). E and I use different slow-period triples.adex_network — two pairs (baseline, N_PAIRS=2), each pair isolated from the other. Its optional three-pair configuration (N_PAIRS=3) adds an excitatory ring E0 -> E1 -> E2 -> E0. The submitted top wrapper fixes N_PAIRS=2; using the stretch configuration also requires widening its wrapper ports.adex_config supplies a reset-defaulted active register bank. SPI writes first update a shadow bank; a separate COMMIT frame transfers every field to the active bank on one core-clock edge. The following controls are runtime configurable.
| Group | Runtime field | Default | Meaning |
|---|---|---|---|
| Per neuron | VTH_Q |
4096 | signed 14-bit spike threshold (max +8191; E0 test uses 5120) |
| Per neuron | IEXT_Q |
1024 | signed 12-bit input-current magnitude (default 1024, tests <=1024) |
| Global | VTRIG_Q |
3072 | signed 14-bit fast-unit trigger |
| Global | VSTEP_Q |
4096 | signed 14-bit subtractive reset step |
| Global | FINC0, FINC1 |
128, 192 | unsigned 9-bit fast-unit increments |
| Global | WBUMP_Q |
256 | unsigned 10-bit bump for each slow unit (default 256, tests <=600) |
| Global | INH_AMT_Q |
512 | unsigned 12-bit reciprocal-inhibition magnitude (default 512, tests <=256) |
The 14-bit V/VTRIG/VSTEP, 12-bit IEXT, and 12-bit INH_AMT field widths are the
demonstrated operating ranges (see src/adex_config.v header); the 14-bit signed
thresholds are required because the E0 phase-locked test raises VTH_Q to 5120
(13-bit signed caps at +4095), and 12-bit signed IEXT is required for +1024
(11-bit signed caps at +1023).
VINIT_Q, KV, KF0/1, FSH0/1, KS0..2, SSH0..2, SLOW_DECAY_SHIFT, and the optional stretch-ring excitation magnitude remain compile-time constants. Keeping shift counts static avoids variable shifters in the neuron datapath.
| Pin | Direction | Function |
|---|---|---|
clk |
in | system clock |
rst_n |
in | active-low reset |
ena |
in | unused |
ui_in[0] |
in | PWM input current, E0 |
ui_in[1] |
in | PWM input current, I0 |
ui_in[2] |
in | PWM input current, E1 |
ui_in[3] |
in | PWM input current, I1 |
ui_in[7:4] |
in | unused |
uio_in[0] |
in | SPI CS_N |
uio_in[1] |
in | SPI mode-0 SCLK |
uio_in[2] |
in | SPI MOSI |
uio_in[7:3] |
in | unused |
uo_out[0] |
out | registered E0 spike indicator |
uo_out[1] |
out | spike I0 |
uo_out[2] |
out | spike E1 |
uo_out[3] |
out | spike I1 |
uo_out[4] |
out | any-spike aggregate (E0 |
uo_out[7:5] |
out | tied low |
uio_out, uio_oe |
out | tied low; this is a write-only SPI interface |
ui_in[7:4], uio_in[7:3], and ena are ignored by the RTL. The four active drive pins must be driven to known binary values. They are sampled once per clock and act as binary current enables; an external source may provide PWM, while a constant high level supplies the configured IEXT_Q every cycle. An unresolved active input can propagate an unknown value through the state update.
rst_n low for at least 5 clock cycles, then release.ui_in[3:0] high (constant PWM = constant input current). E.g. drive all four high.uo_out[0..3] is high after a clock edge when the corresponding block's pre-update v was greater than VTH_Q; it is not edge-detected. uo_out[4] is the OR of those four registered indicators.CS_N low, send one or more 32-bit MSB-first SPI mode-0 write frames, then send a COMMIT frame. SCLK must be no faster than clk/8; keep CS_N stable for at least two clk cycles before and after each frame.| Frame | Bits [31:28] | Bits [27:24] | Bits [23:20] | Bits [19:4] | Bits [3:0] |
|---|---|---|---|---|---|
| Write | 0xA |
target 0=E0, 1=I0, 2=E1, 3=I1, F=global | field ID | 16-bit value | 0 |
| Commit | 0xC |
0 |
0 |
0 |
0 |
Per-neuron fields are 0=VTH_Q, 1=IEXT_Q. Global fields are 0=VTRIG_Q, 1=VSTEP_Q, 2=FINC0, 3=FINC1, 4=WBUMP_Q, and 5=INH_AMT_Q. Unsigned fields use the least-significant 9, 11, or 15 bits of the value field as applicable.
With the default parameters, the intended pin-level checks are:
These are behavioural checks, not validated numerical characterisation. The RTL has been re-verified with the full 14-test suite (see Verification scope below).
The automated suite is test/test.py (cocotb, 14 tests: reset state, directed E0 block arithmetic against a Python fixed-point reference, SPI shadow/commit behavior, silence, spiking + aggregate OR, adaptation ratio, tonic f-I response, fast spiking, inhibition suppression, pair isolation, no-lock, phase-locked alternation, bursting pattern, and burst length vs WBUMP). The arithmetic test uses nine directed vectors and exercises period-counter wrap; the SPI test checks that a write has no effect before commit and reaches E0 after commit. Run with make -B in test/. Three tests reach into internal hierarchy (dut.net.pair0.e_block or dut.u_config); at gate level the netlist can flatten that hierarchy, so their internal checks log a warning and return. test_reset_state still checks that the visible outputs are zero in reset.
tt mamba environment), driven by make -B in test/.TESTS=14 PASS=14 FAIL=0 SKIP=0). Measured spike metrics from this run: basic spiking E0=627, I0=376, E1=328, I1=329 over 6000 cycles with the aggregate-OR check clean; adaptation ISI head8=6.5 -> tail100=9.2 (E0 fired 872 times); tonic f-I IEXT=512->241 spikes/ISI 8.28 vs IEXT=1024->424 spikes/ISI 4.71; fast spiking 997 spikes/ISI 2.00 with max high-run 1 (pulse-clean); inhibition E0 alone=437, with-I0=419, after=435 while I0 fired 252; pair isolation drove pair 0 giving [E0,I0,E1,I1]=[315,191,0,0]; no-lock coincidence fraction=0.42 (threshold 0.5); phase-locked alternation E=399/I=400 at ISI (5.00, 5.00) with disjoint alternating one-cycle pulses; bursting 852 spikes/166 bursts, avg burst size 5.1, avg inter-burst gap 73.9 vs intra-burst ISI 4.1; WBUMP sweep avg burst size 5.1 (WBUMP=200) -> 2.0 (WBUMP=600).test_reset_state, test_arith_block, test_spi_shadow_commit) access internal hierarchy; at gate level these log a warning and return without checking internals. The cocotb run emits deprecation warnings from the testbench's use of the older units=/binstr/signed_integer APIs under cocotb 2.0.1; they are cosmetic and do not affect any assertion.verilator --lint-only -Wall (Verilator 5.050) on the full five-source set (project.v, adex_config.v, adex_block.v, adex_pair.v, adex_network.v) is warning-clean — zero warnings, exit 0.(spike_now ? wbump_14 : 0) term into one shared wbump_term_14 wire across the three slow-unit accumulators.sat16 helper input from 20-bit to 18-bit (the prime accumulator v_sum is 18-bit; the dropped bits were pure sign-extension).wbump_q16 widen wire, and dropped the unreachable negative branch of slow_relax (the slow units start at 0 and only ever relax toward zero, so their state is provably non-negative).
All of these preserve every state the reference model drives; the arithmetic lock test_arith_block still passes and the full five-source set now lints -Wall clean with zero warnings. Base area is roughly 78% of the 2x2 core by local yosys estimate (behaviour-identical to the pre-reduction baseline); final density is settled by the shuttle's OpenROAD place-and-route run, not by further RTL shaving.clk, so it is intended for slow configuration traffic only (SCLK <= clk/8), not a high-speed independent SPI clock.N_PAIRS=3 stretch branch reuses E0/I0 runtime controls for pair 2; the submitted wrapper is fixed at N_PAIRS=2.v/f/w states are not exposed (the old debug bus is gone).src/adex_neuron_system_tt_lut32.v is the deprecated Q8.7 LUT-based core from the earlier iteration. It contains LUT, multiplication, and division logic, is not in info.yaml's source list, and is not synthesised.N/A. Self-contained digital core; no external components required.
| # | Input | Output | Bidirectional |
|---|---|---|---|
| 0 | PWM_E0 | spike_E0 | SPI_CS_N |
| 1 | PWM_I0 | spike_I0 | SPI_SCLK |
| 2 | PWM_E1 | spike_E1 | SPI_MOSI |
| 3 | PWM_I1 | spike_I1 | |
| 4 | any_spike | ||
| 5 | |||
| 6 | |||
| 7 |