-
Notifications
You must be signed in to change notification settings - Fork 0
Modules Reference
This document covers all VHDL modules in the project. The current board bring-up
targets are pix16_sbc_sd_boot_top -> sbc_t65_sdram_boot_top and
tang20k_sbc_top -> sbc_t65_boot_monitor_top. The smaller
pix16_sbc_minimal_top -> sbc_minimal_top path remains a useful smoke test.
Modules marked (inactive) are present for reference or simulation but are not
included in the current SD boot build.
boards/pix16/rtl/
└── pix16_sbc_sd_boot_top.vhd — PIX16 SD/SDRAM/VGA/UART board wrapper
rtl/core/
├── sbc_pkg.vhd — shared types, memory map, constants
├── bus_decode.vhd — address → device selection
├── sbc_t65_sdram_boot_top.vhd — SBC core with SDRAM + shadow ROM
│ ├── boot/boot_debug_uart.vhd — serial boot-status output
│ ├── boot/boot_vga_debug.vhd — VGA boot/status/RAM-test screen
│ ├── boot/boot_sdram_test.vhd — SDRAM self-test before CPU release
│ ├── boot/sd_rom_loader.v — SD-sector loader into shadow ROM
│ ├── boot/uart_debug_monitor.vhd — UART machine monitor and hex loader
│ ├── cpu/t65_adapter.vhd — T65 CPU wrapper (+ RDY bus-steal port)
│ │ └── third_party/t65/ — external T65 6502 core
│ ├── mem/sync_ram.vhd — ZP/stack RAM and text VRAM
│ ├── mem/boot_shadow_rom.vhd — writable 16 KB ROM window at $C000-$FFFF
│ ├── mem/sdram_if.vhd — byte interface to board SDRAM controller
│ ├── mem/sdram_ctrl.vhd — board SDRAM command/data controller
│ ├── mem/char_rom.vhd — 8×8 character patterns
│ ├── peripherals/via6522.vhd — VIA 6522: Timer 1 IRQ + Port B
│ ├── peripherals/uart6551.vhd — UART 6551: CPU TX/RX registers
│ └── peripherals/vic_vga.vhd — VIC: bus stealing + VGA output
third_party/alinx_sd/ — vendor SD-card SPI sector core
boards/tang_primer_20k/rtl/
├── tang20k_sbc_top.vhd — Tang HDMI/CH340/on-board-SD board wrapper
└── tang20k_hdmi_tx.vhd — Gowin rPLL + TMDS output wrapper
rtl/core/
├── sbc_pkg.vhd — shared types, memory map, constants
├── bus_decode.vhd — address → device selection
├── sbc_t65_boot_monitor_top.vhd — SBC core with internal BSRAM + shadow ROM
│ ├── boot/boot_debug_uart.vhd — serial boot-status output
│ ├── boot/boot_vga_debug.vhd — HDMI boot/status screen
│ ├── boot/sd_rom_loader.v — SD-sector loader into shadow ROM
│ ├── boot/uart_debug_monitor.vhd — UART machine monitor and hex loader
│ ├── cpu/t65_adapter.vhd — T65 CPU wrapper
│ ├── mem/sync_ram.vhd — ZP/stack RAM, main RAM, and text VRAM
│ ├── mem/boot_shadow_rom.vhd — writable 16 KB ROM window at $C000-$FFFF
│ ├── mem/char_rom.vhd — 8×8 character patterns
│ ├── peripherals/via6522.vhd — VIA 6522: Timer 1 IRQ + Port B
│ ├── peripherals/uart6551.vhd — UART 6551: CPU TX/RX registers
│ ├── peripherals/vic_vga.vhd — VIC: bus stealing + VGA/HDMI pixel output
│ ├── peripherals/sound_voice.vhd — single-voice synth (channel 0, $8830)
│ └── peripherals/pt8211_dac.vhd — PT8211 audio DAC I2S serializer
third_party/alinx_sd/ — vendor SD-card SPI sector core
rtl/core/
└── sbc_minimal_top.vhd — compact T65/VGA core
boards/pix16/rtl/
└── pix16_sbc_minimal_top.vhd — PIX16 wrapper for the minimal top
fpga/sw/
├── rom_demo.s — ca65 6502 assembly source (kernel + ISR + strings)
├── rom_demo.cfg — ld65 linker config: 2 KB ROM at $F800
├── bin_to_fpga_hex.py — binary → FPGA sim hex converter (skips 0xEA fill)
├── upload_rom_demo.py — UART monitor upload script for rom_demo.bin
└── Makefile — make all → installs ../sim/hex/rom_welcome.hex
make upload-demo → uploads rom_demo.bin via UART
make sd-ehbasic → builds 16 KB EhBASIC ROM + SD image
fpga/tools/
├── build_fpga_ehbasic.py — patches + assembles EhBASIC; links with kernel
│ --sd-image also produces SD card boot image
└── upload_monitor_hex.py — UART monitor upload for any .rom file
fpga/roms/
├── fpga_ehbasic_16kb.rom — 16 KB output: kernel ($C000) + EhBASIC ($D000)
└── fpga_ehbasic_16kb.img — raw SD boot image (512 B header + 16 KB payload)
tools/kernel/
└── kernel.s — 4 KB kernel ROM ($C000-$CFFF)
jump table, CHROUT/CHRIN, CLRSCR, SCROLL
to_upper: a-z → A-Z in CHRIN_NB and CHROUT
Build commands (from project root):
python fpga/tools/build_fpga_ehbasic.py # ROM only
python fpga/tools/build_fpga_ehbasic.py --sd-image # ROM + SD boot image
python fpga/tools/build_fpga_ehbasic.py --upload --run --verbose # ROM + UART upload
make -C fpga/sw sd-ehbasic # same as --sd-image via makefpga/tools/
└── gen_petscii_char_rom.py — regenerates rtl/mem/char_rom.vhd from Python
Reads existing ROM, replaces $60-$7F with
PETSCII-style block/line graphics, writes
the full VHDL file back.
Run: python fpga/tools/gen_petscii_char_rom.py
rtl/core/
├── sbc_t65_top.vhd — full SBC (VIA + UART + VIC core)
├── sbc_top.vhd — test-mode SBC (ROM-scripted CPU slot)
├── peripherals/vic_core.vhd — old dual-port VIC (replaced by vic_vga)
├── peripherals/vic_pixel_gen.vhd — old pixel generator (replaced by vic_vga)
└── peripherals/reg_stub.vhd — generic register placeholder
boards/pix16/rtl/
├── pix16_top.vhd — old PIX16 top (test_runner + vic_core)
└── pix16_board.vhd — old board integration
Central definitions used by every other module.
subtype addr_t is std_logic_vector(15 downto 0);
subtype data_t is std_logic_vector(7 downto 0);
type device_sel_t is (
DEV_NONE, DEV_SRAM, DEV_VIC_TEXT, DEV_VIA, DEV_UART,
DEV_DISK, DEV_VIC_BLIT, DEV_VIC_SPR, DEV_SOUND0..3,
DEV_VIC_SPD, DEV_VIC_REG, DEV_VIC_BMP, DEV_ROM
);
function in_range(addr, first, last) return boolean;The complete minimal system: CPU, internal zero-page/stack RAM, SRAM, VRAM, ROM, VIA, UART, VIC, and VGA output.
Ports:
entity sbc_minimal_top is
generic (ROM_INIT_FILE : string := "");
port (
clk, reset_n : in std_logic;
vga_r : out std_logic_vector(4 downto 0);
vga_g : out std_logic_vector(5 downto 0);
vga_b : out std_logic_vector(4 downto 0);
vga_hs, vga_vs : out std_logic;
dbg_cpu_addr : out addr_t;
dbg_cpu_data : out data_t;
dbg_cpu_din : out data_t;
dbg_cpu_we : out std_logic;
dbg_cpu_sync : out std_logic
);
end entity;Key signals:
| Signal | Description |
|---|---|
cpu_enable |
Toggles every clock — 27 MHz T65 rate from the Tang's 54 MHz system clock |
cpu_bus_we |
cpu_we AND NOT cpu_enable — write on stable half-cycle |
cpu_rdy |
NOT vic_stealing — CPU halted during VIC bus steal |
vic_stealing |
High for 82 system clocks during each H-blank (40 chars + 40 colours, each with setup) |
vram_addr |
Mux: vic_addr[10:0] during steal, else cpu_addr[10:0]
|
vram_we_mux |
Zero during steal (VIC only reads), else vram_we
|
zp_cs |
Selects internal FPGA RAM for $0000-$01FF
|
zp_we |
Write enable for zero-page/stack RAM |
Memory instances:
| Instance | Module | Generic | Address |
|---|---|---|---|
zp_ram_i |
sync_ram |
ADDR_WIDTH=9, ASYNC_READ=true | $0000–$01FF |
sram_i |
sync_ram |
ADDR_WIDTH=12, ASYNC_READ=true | $0200–$0FFF |
vram_i |
sync_ram |
ADDR_WIDTH=11, ASYNC_READ=true | $8000–$87FF |
rom_i |
rom |
ADDR_WIDTH=11, ASYNC_READ=true | $F800–$FFFF |
bus_decode still maps $0000-$7FFF to DEV_SRAM; sbc_minimal_top splits
that logical selection internally so zero-page and stack traffic use zp_ram_i
while the rest of the minimal RAM range uses sram_i.
Thin wrapper that exposes the exact ports required by pix16.ucf. Instantiates
sbc_minimal_top and passes VGA signals through. SDRAM pins are absent (not in UCF).
entity pix16_sbc_minimal_top is
generic (ROM_INIT_FILE : string := "../../../sim/hex/rom_welcome.hex");
port (
clk, reset_n : in std_logic;
vga_out_r : out std_logic_vector(4 downto 0);
vga_out_g : out std_logic_vector(5 downto 0);
vga_out_b : out std_logic_vector(4 downto 0);
vga_out_hs, vga_out_vs : out std_logic;
key : in std_logic_vector(3 downto 0);
led : out std_logic_vector(1 downto 0)
);
end entity;led(0) is hardwired high (power indicator). led(1) mirrors NOT key(0).
Adapts the T65 24-bit bus to the 16-bit system bus and exposes the RDY pin for
bus stealing.
entity t65_adapter is
port (
clk, reset_n : in std_logic;
enable : in std_logic; -- clock gate (div-2)
rdy : in std_logic := '1'; -- '0' halts CPU (bus steal)
irq_n, nmi_n : in std_logic;
data_in : in data_t;
addr : out addr_t;
data_out : out data_t;
we : out std_logic;
sync : out std_logic
);
end entity;rdy defaults to '1' so existing instantiations without the port remain valid.
The T65 advances only when both enable = '1' AND rdy = '1'.
Write enable derivation: we = (NOT t65_r_w_n) AND t65_vda.
Single-port block RAM. Used for zero-page/stack RAM, CPU SRAM, and VRAM.
entity sync_ram is
generic (
ADDR_WIDTH : positive := 15;
ASYNC_READ : boolean := false
);
port (
clk : in std_logic;
we : in std_logic;
addr : in std_logic_vector(ADDR_WIDTH-1 downto 0);
din : in data_t;
dout : out data_t
);
end entity;With ASYNC_READ = true the read path is combinational (write-through on we = '1').
The current SD boot top uses synchronous VRAM (ASYNC_READ = false) so XST can
infer a compact RAM structure. vic_vga therefore includes a one-cycle read
latency pipeline during bus stealing.
16 KB RAM for the CPU ROM window $C000-$FFFF.
Two write sources share the load port in sbc_t65_sdram_boot_top:
- SD boot loader during reset,
- UART monitor when the CPU is held.
The CPU sees it as ROM, but the monitor can patch it live for development.
Loaded at synthesis from a hex file. Format: one XXXX YY entry per line
(4-digit hex offset, 2-digit hex byte, no comments — comments cause synthesis errors).
Default fill: 0xEA (NOP). Reset vector placed at the last two entries of the 2 KB window:
07FC 00 ; reset vector low → $F800
07FD F8 ; reset vector high
1 KB combinational ROM. 128 characters × 8 rows of 8 pixels.
Address: char_code[6:0] & row[2:0]. Output: 8-bit pixel row (bit 7 = leftmost pixel).
| Range | Content |
|---|---|
$00–$1F |
PETSCII screen-code letters (A–Z mapped to codes 1–26) |
$20–$5F |
ASCII punctuation, digits, uppercase A–Z |
$60–$7F |
PETSCII block/line graphics: horizontal/vertical bars, corners, T-pieces, cross, diagonals, checkerboard, quadrants, diamond, ball, disk, arrows, full block |
Bit 7 of the character code selects reverse-video rendering in both vic_vga and
boot_vga_debug. $80–$FF display as inverted versions of $00–$7F.
Important — lowercase conflict: Standard ASCII lowercase letters a–z occupy codes
$61–$7A, which overlap the PETSCII block-graphics range. Writing a lowercase code
to VRAM intentionally renders a block graphic instead of a letter. The kernel keeps
normal text readable by converting characters a–z → A–Z in CHRIN_NB and CHROUT.
BASIC programs that need raw PETSCII graphics write $60-$7F directly to VRAM.
The checked-in PETSCII BASIC demo at examples/petscii_gfx.bas follows that rule:
it writes labels and glyphs directly to $8000 text VRAM with short POKE lines,
instead of relying on PRINT CHR$() or string slicing. This also makes the demo a
stress test for CPU writes to the shared VRAM while the VGA/HDMI scanout is active.
The VHDL source is generated by fpga/tools/gen_petscii_char_rom.py. Run the
script after modifying any character pattern to regenerate char_rom.vhd.
Replaces the old vic_core + vic_pixel_gen pair. Handles video timing, bus stealing,
character rendering, and VGA signal generation in a single module.
Ports:
entity vic_vga is
port (
clk, reset_n : in std_logic;
-- Bus steal interface
vic_addr : out addr_t; -- VRAM address to read
vram_data : in data_t; -- synchronous VRAM output, 1-cycle latency
vic_stealing : out std_logic; -- '1' = CPU halted, VIC has bus
-- Character ROM
char_addr : out std_logic_vector(9 downto 0);
char_data : in data_t;
-- VGA output
vga_hs, vga_vs : out std_logic;
vga_r : out std_logic_vector(4 downto 0);
vga_g : out std_logic_vector(5 downto 0);
vga_b : out std_logic_vector(4 downto 0)
);
end entity;Internal state:
| Signal | Description |
|---|---|
pce |
Pixel clock enable — divides the Tang's 54 MHz system clock to 27 MHz |
hc, vc
|
Horizontal / vertical scan counters (pixel clock units) |
linebuf |
40-byte register array — one char code per column |
fetching |
High during the 41-cycle bus steal window |
fetch_col |
Current VRAM address column being presented (0–39) |
fetch_store_col |
Current line-buffer column being filled from the previous cycle's VRAM data |
fetch_valid |
Low for the first setup cycle, high while returned VRAM data is valid |
fetch_row |
Character row being prefetched for the next scan line |
Bus steal timing:
The steal begins on the pixel clock edge where hc = H_VISIBLE - 1 = 639 (last visible pixel).
The first stolen cycle presents the address for column 0. Each following cycle
stores the previous cycle's synchronous VRAM data and presents the next address.
-
vic_stealingasserted → CPU halted viaRDY. -
vic_addrdriven to$8000 + fetch_row × 40 + fetch_col. - On the next clock,
vram_datais stored intolinebuf(fetch_store_col). - The steal ends after column 39 has been stored.
CPU VRAM writes during bus steal:
The active boot cores use single-port text VRAM, so a CPU write can collide with a
VIC prefetch. vram_we_mux is still forced low while vic_stealing = '1', but a
CPU write pulse in that window is now captured into vram_wr_pending,
vram_wr_addr, and vram_wr_data. When the steal ends, the mux selects the
latched address/data for one clock and asserts vram_we_mux; cpu_rdy remains low
for that commit cycle. This fixes the previously observed symptom where repeated
BASIC POKE runs left random stale PETSCII characters in the box demo.
Display pipeline (combinational from scan counters):
hc, vc → col, char_row, char_line, char_px → linebuf(col) → char_addr
→ char_rom_data → pixel_bit → vga_r/g/b
All combinational — no additional pipeline latency on top of the line-buffer prefetch.
Combinational VGA text renderer active during SD load and SDRAM self-test.
Displays a fixed 40×25 character grid using the same char_rom as vic_vga.
Screen layout:
| Row | Content |
|---|---|
| 1 | PETSCII block/line graphics $60–$7F (32 glyphs, cols 4–35) |
| 2 | **** 6502 SINGLE BOARD COMPUTER **** |
| 4 |
SD BOOT or SD ERR + SDRAM test status |
| 6 | BEREIT. |
| 8 |
VIA-T1: XX (live tick counter) |
| 22 |
GFX: label + all 32 block-graphics glyphs ($60–$7F, cols 6–37) |
Character assignment is fully combinational over the crow/col scan-counter
signals using a case crow is structure. Each character row is a separate when
branch; the put_str helper writes a string literal into consecutive columns.
Bit 7 of any character code selects reverse video.
UART monitor entered by the board button. It stops the CPU, accepts machine
monitor commands, and issues single-byte memory transactions through
monitor_mem_*.
Core command set:
| Command | Purpose |
|---|---|
M addr [end] |
Hex dump with ASCII column |
D addr [end], U addr [end]
|
Disassemble |
E addr byte, W addr byte, : addr byte
|
Write one byte |
L addr |
Hex loader for sequential bytes |
G [addr] |
Resume or restart at address |
H, ?
|
Help |
See UART Monitor for the operator workflow and upload tool.
VGA timing constants (640×480 @ ~60 Hz, 50 MHz clock):
| Parameter | Value |
|---|---|
| H_TOTAL | 800 pixel clocks |
| V_TOTAL | 525 lines |
| H_SYNC pulse | clocks 656–751 (active low) |
| V_SYNC pulse | lines 490–491 (active low) |
| Text area V | lines 40–439 (40 px top/bottom border) |
| Char size | 16×16 screen pixels (2× scaled 8×8) |
Purely combinational. Maps a 16-bit CPU address to device_sel_t.
entity bus_decode is
port (addr : in addr_t; sel : out device_sel_t);
end entity;Used by sbc_minimal_top to gate SRAM writes, VRAM writes, and ROM reads.
During a VIC bus steal, cpu_addr is stable (CPU halted), so dev_sel is also stable.
Original VIC with dual-port text RAM (CPU write port + pixel generator read port).
Replaced by vic_vga because the Spartan-6 dual-port block RAM inference produced
no VGA signal in hardware.
Standalone pixel generator that consumed the text RAM read port of vic_core.
Merged into vic_vga.
VIA 6522 — parallel I/O + dual 16-bit timers + interrupt flags.
Used by sbc_minimal_top for Timer 1 IRQ and Port B (LED blink).
Key registers used by the demo kernel:
| Register | Offset | Purpose |
|---|---|---|
| ORB | $00 | Port B output latch (bit 0 → LED) |
| DDRB | $02 | Port B direction ($FF = all output) |
| T1CL | $04 | Counter low — read clears T1 IRQ flag |
| T1CH | $05 | Counter high — write loads latches and starts timer |
| T1LL/T1LH | $06/$07 | Timer 1 latches (auto-reload value) |
| ACR | $0B | Bit 6 = 1 → free-running mode |
| IER | $0E | Bit 7 = set/clear, bit 6 = T1 enable |
UART 6551 — TX/RX data path, status register (TDRE/RDRF/OVR), RX interrupt.
Used by sbc_minimal_top for TX-only in the demo (RX tied to zero).
Writing to the DATA register ($8810) pulses tx_valid and updates tx_data.
The board wrapper feeds the serializer busy signal back to tx_busy, so TDRE
is clear while a byte is actively being transmitted and firmware can safely emit
multi-line reset diagnostics.
Single-voice sound synthesizer (channel 0, $8830–$8839). A 24-bit
phase-accumulator oscillator produces square and noise (16-bit Galois LFSR)
waveforms; frequency (direct in Hz) and 8-bit volume are honoured, with the note
gated by CONTROL bit 0. The duration and ADSR registers are accepted (so 6502
code writing the full 10-register block does not error) but are not yet acted on.
The register layout mirrors src/soundchip.h, so it is software-compatible with
the C emulator's sound chip. Output is a signed 16-bit sample. Verified by
sim/tb/tb_sound_voice.vhd.
I2S serializer for the dock board's PT8211 (TM8211) audio DAC. A 1:1 VHDL port of
Sipeed's proven pt8211_drive.v: a free-running ~1.5 MHz BCK, MSB-first data with
the PT8211's right-justified WS alignment, fed the signed 16-bit sample from
sound_voice on both channels. Pins on the Tang Primer 20K dock are
dac_bck=N15, dac_ws=P16, dac_din=P15, plus the amplifier-enable pa_en=R16
(tied high in tang20k_sbc_top.vhd). See Sound Chip for the full
register map, usage, and wiring.
The large sound-chip version — an independent second implementation that
ports the complete C-emulator model (src/soundchip.c). sound_voice_full is
one full voice: 5 waveforms (sine LUT, square, sawtooth, triangle, xorshift32
noise), a time-based ADSR envelope, note duration, and per-voice volume.
sound_chip4 wraps four of them with a clipping mixer and one chip-select per
voice. Register-compatible with the bring-up sound_voice, so 6502 code is
portable between versions. It is wired into the Tang Primer 20K board top and
drives the PT8211 DAC. Verified by sim/tb/tb_sound_chip4.vhd. See
Sound Chip → The Large 4-Voice Version.
rom_demo.s prints reset-time state over UART before interrupts are enabled:
[RESET] 6502 SBC DEBUG
...
SYS CHECK
ZP OK
STK OK
RAM OK
VRAM OK
VIA OK
UART OK
CHECK DONE, CLI NEXT
The memory probes write $AA and $55 to one representative byte in each
region, verify the readback, and then restore the original byte. The check runs
before CLI, so VIA Timer 1 cannot interrupt the probe sequence.
Verifies that the T65 CPU executes the kernel and writes the expected 24 ASCII bytes
of "WILLKOMMEN ZUM 6502 SBC!" to VRAM addresses $8000–$8017 via the CPU debug bus.
Times out with failure if not all characters arrive within 5000 clock cycles.
See Also:
Generated from 6502-sbc-fpga Markdown documentation. Part of the 6502 SBC emulator project (emulator Wiki).