33 Simple Signal Generator

33 : Simple Signal Generator

Design render
  • Author: Matt Venn
  • Description: Square wave generator with a phase-shifting second channel for metastability experiments
  • GitHub repository
  • Open in 3D viewer
  • Clock: 50000000 Hz

How it works

Two square wave generators drive uo[1:0].

Channel 0 (uo[0]): independent square wave. High-time (on_count) and low-time (off_count) are set independently via SPI, giving full control over frequency and duty cycle.

Channel 1 (uo[1]): phase-shifted replica of ch0. It uses the same on_count and off_count as ch0, but its rising edge is offset by a programmable delay. This is designed for exploring metastability on an SR latch: by placing ch1's edge close to ch0's edge, the two inputs can be brought arbitrarily close together.

Phase control

The total delay applied to ch1's rising edge is:

total_delay = spi_offset + enc_int + sigma_delta_carry

spi_offset (registers 4–5): signed 16-bit static offset in clock cycles, set via SPI. Positive = ch1 lags ch0; negative = ch1 leads ch0. Range: ±32767 cycles.

Encoder (ui[4]=A, ui[5]=B): a quadrature encoder adjusts the phase in real time. Each click changes the phase by enc_step / 256 cycles (Q8 fixed-point). The integer part of the encoder accumulator is added directly to the delay; the fractional part is dithered via first-order sigma-delta modulation, so sub-cycle offsets are averaged accurately over multiple periods. Encoder range: ±127 cycles (use spi_offset for coarse positioning).

enc_step (register 10): step size per encoder click in 1/256-cycle units. Examples:

  • enc_step = 1 → ~0.004 cycles/click (~78 ps at 50 MHz), finest resolution
  • enc_step = 64 → 0.25 cycles/click
  • enc_step = 128 → 0.5 cycles/click
  • enc_step = 255 → ~1 cycle/click

Encoder button (ui[2], PmodEnc's SWT pin, active-high): while held, multiplies enc_step by 8x for fast scanning across the phase range. Releasing it reverts the very next click to the normal step size.

ch1's delay (spi_offset + enc_int + sigma_delta_carry) covers the full [0, period) range with no clamping — it wraps modulo the period, so ch1's pulse can land anywhere relative to ch0's, including spanning the period boundary. (For very small periods combined with large spi_offset/encoder values — larger in magnitude than the period — the wrap is computed mod one period only, so the resulting delay may not match the mathematical mod for |total_delay| >= period; this is a pre-existing edge case unrelated to normal use.)

Frequency formula

At 50 MHz, for a 50% duty-cycle square wave at frequency F:

on_count = off_count = 25_000_000 // F

Both counts are 16-bit → minimum frequency ≈ 382 Hz (on_count = off_count = 65535).

Setting on_count = 0 silences both channels.

The SPI peripheral is reused from calonso88/tt07_alu_74181.

How to test

The SPI master (e.g. RP2350 running MicroPython) programs the design using machine.SoftSPI. Each SPI frame is 2 bytes:

  • Byte 0: 0x80 | reg_addr (write), or reg_addr (read)
  • Byte 1: data

SPI mode: CPOL and CPHA are set via ui[0] and ui[1] respectively (both 0 for mode 0).

Register map

Reg Field Notes
0 ch0 on_count[15:8] MSB
1 ch0 on_count[7:0] LSB
2 ch0 off_count[15:8] MSB
3 ch0 off_count[7:0] LSB
4 spi_offset[15:8] Signed 16-bit MSB; +ve=lag, −ve=lead
5 spi_offset[7:0] Signed 16-bit LSB
6 enc_step[7:0] Q8 step per encoder click (0–255)
7 reserved

MicroPython example — 10 kHz, 500-cycle lag, encoder fine-tune

from machine import Pin, SoftSPI

spi = SoftSPI(baudrate=100_000, polarity=0, phase=0, bits=8,
              firstbit=SoftSPI.MSB,
              sck=Pin(30), mosi=Pin(31), miso=Pin(28))
cs = Pin(29, Pin.OUT, value=1)

def write_reg(addr, val):
    cs(0); spi.write(bytes([0x80 | addr, val])); cs(1)

# ch0: 10 kHz, 50% duty (on=2500, off=2500 @ 50 MHz)
write_reg(0, 0x09); write_reg(1, 0xC4)  # on_count  = 2500
write_reg(2, 0x09); write_reg(3, 0xC4)  # off_count = 2500

# ch1: 500-cycle static lag (10 µs)
offset = 500  # cycles
write_reg(4, (offset >> 8) & 0xFF)
write_reg(5,  offset       & 0xFF)

# Encoder: 0.25 cycles/click for fine phase adjustment
write_reg(10, 64)

For RP2350 on the TT demo board, scripts/demo.py provides ready-made helpers (set_ch0, set_ch0_counts, set_phase_offset, set_enc_step, silence). Use scripts/run_freq.py to program from the command line:

python scripts/run_freq.py 1000 --offset 500 --enc-step 64

Encoder hardware

Connect a Digilent PModEnc to the bottom row of the input Pmod connector on the Tiny Tapeout demo board. This maps the encoder A/B outputs to ui[4] and ui[5], and the encoder's pushbutton (SWT) to ui[2] (fast-scan, see "Phase control" above).

Testing

Simulation (cocotb)

source /home/matt/oss-cad-suite/environment
cd test && make
Test What it checks
test_spi_registers Round-trip read/write of all config registers
test_frequency_generation ch0 outputs correct frequency (edge count)
test_ch1_inphase offset=0 → ch1 fires simultaneously with ch0
test_spi_phase_offset Positive SPI offset → ch1 lags ch0 by correct cycle count
test_channel_silence on_count=0 holds both outputs LOW
test_encoder_integer_phase Encoder clicks accumulate to integer cycle offset
test_encoder_sigma_delta Fractional enc_step dithers delay via sigma-delta
test_encoder_button_fast_scan Holding the encoder button multiplies enc_step by 8x
test_encoder_fast_scan_clamp_no_wrap Holding the button at ENC_MAX/ENC_MIN clamps correctly instead of wrapping (16->17-bit overflow fix)

Hardware-in-the-loop (HIL)

test/test_hil.py verifies frequency accuracy on real hardware using a Keysight oscilloscope and the RP2350. Probes on uo[0] (CH1) and uo[1] (CH2).

External hardware

  • RP2350 (or any SPI master) connected to uio[6:4] (MOSI, CLK, CS_N) and uio[3] (MISO)
  • Digilent PModEnc on the bottom row of the input Pmod connector (→ ui[4]=A, ui[5]=B, ui[2]=button/SWT for 8x fast-scan)
  • Oscilloscope on uo[1:0]
  • SR latch with S=uo[0] and R=uo[1] (or vice versa) for metastability experiments

IO

#InputOutputBidirectional
0SPI CPOLChannel 0 square wave output
1SPI CPHAChannel 1 square wave output
2Encoder button (hold for 8x fast-scan)
3SPI MISO
4Encoder ASPI CS_N (active low)
5Encoder BSPI CLK
6SPI MOSI
7

Chip location

Controller Mux Mux Mux Mux Mux Analog Mux Analog Mux Mux Mux Mux Analog Mux Mux Mux Analog Mux Mux Mux Mux tt_um_chip_rom (Chip ROM) tt_um_factory_test (Tiny Tapeout Factory Test) tt_um_tnt_gf_555 (tnt's recreation of a 555 on GF180mcu) tt_um_mcml_vco (MCML VCO) tt_um_tnt_gf_vco (Quadrature VCO) tt_um_oscillating_bones (Oscillating Bones) tt_um_MichaelBell_tinyQV (TinyQV Risc-V SoC) tt_um_essen (256x8 SRAM) tt_um_analog_factory_test (TT GF180mcuD Analog Factory Test) tt_um_cw_testbuffer (TT GF180mcuD Testbuffer) tt_um_fabulous_gf_0p3 (Tiny FABulous FPGA) tt_um_fabulous_gf_0p3_3v3 (Tiny FABulous FPGA (3.3V version)) tt_um_htfab_momcap (MoM capacitor) tt_um_gf_r2r_dac (GF R2R DAC) tt_um_AbAdA_2048 (Abad2048) tt_um_mattvenn_signal_generator (Simple Signal Generator) tt_um_74hct00 (74HCT00 Quad 2-Input NAND (3.3 V)) tt_um_algofoogle_gf_analog (algofoogle analog stuff) tt_um_LPCAS_TTGF0P3_TP1 (3.3V Folded Cascode OTA ) tt_um_kianV_rv32ima_uLinux_SoC (KianV uLinux SoC) tt_um_kiterai_current_dac (Analog 4 bit Current DAC) tt_um_urish_simon (Simon Says memory game) tt_um_waferspace_vga_screensaver (Wafer.space Logo VGA Screensaver) tt_um_urish_usb_cdc (USB CDC (Serial) Device) tt_um_a1k0n_nyancat (VGA Nyan Cat) tt_um_rahulbhagwat_gf180_bandgap_reference (GF BGR) tt_um_odgrip_polywave (PolyWave) tt_um_2048_vga_game (2048 sliding tile puzzle game (VGA)) tt_um_combined (Philippine flag waving) tt_um_gojimmypi_ttgfa_UART_FSM_TRNG_Lab (Hardware Entropy Explorer: UART/SPI TRNG and PUF) tt_um_gojimmypi_ttgfa_UART_FSM_TRNG_Lab_analog (Hardware Entropy Explorer: UART/SPI TRNG and PUF (Analog)) tt_um_coldbrew (Until heat death do us part) Available Available Available Available Available Available Available Available Available Available Available Available Available Available Available Available Available Available Available Available Available Available Available Available Available Available Available Available Available Available Available Available Available Available Available Available Available Available Available Available Available Available Available Available Available