This project implements a small, programmable DMA-style controller that receives bytes from an asynchronous UART and writes them into memory. The design is split into three main parts:
uart_rx: Asynchronous UART receiver (8N1) producingrx_dataand a one-clockrx_donepulse.dma_ctrl: DMA controller that waits forrx_done, writes received bytes into consecutive RAM addresses, and stops after a programmable number of bytes (block_size).top: A top-level wrapper tying the UART receiver and DMA controller together and providingdone_interruptwhen the transfer completes.
The included testbench (tb_top.v) simulates a serial RX line, sends 16 bytes (0xA1..0xB0), dumps waveforms to dump.vcd, and verifies that RAM contains the expected data.
- Asynchronous UART: 8N1 UART RX with midpoint sampling using a 100 MHz clock input.
- Variable Block Size DMA: Transfer length is controlled by an 8-bit
block_sizeinput. - RAM Storage: Bytes are written into consecutive RAM addresses; the address increments on each received byte.
- Icarus Verilog (for compilation and simulation)
- GTKWave (for viewing
dump.vcd) - Cursor (for development)
Below is the GTKWave output showing the UART receiving data and the DMA controller writing it to sequential RAM addresses. Note the done_interrupt firing after the final byte.
Metastability & Synchronization: Since the external rx signal is asynchronous to the 100 MHz system clock, I implemented a 2-stage flip-flop synchronizer in the uart_rx module. This prevents metastable states from propagating into the FSM logic.
Precise Mid-bit Sampling: To ensure data integrity at 9600 baud, I designed a clock-divider-based tick generator that samples the incoming serial line at exactly 1.5 bit times after the start-bit edge, effectively capturing the data at its most stable midpoint.
Modular Flow Control: By using a FIFO buffer between the UART and the DMA, I decoupled the data reception from the memory write cycles. This ensures that even if the system bus is busy, no incoming UART bytes are dropped.
FSM-Based DMA Logic: The DMA was designed as a finite state machine to strictly manage the Request-Write-Increment cycle, ensuring that ram_we (Write Enable) is only pulsed when valid data is ready and the address pointer is correctly aligned.
-
Compile and simulate From the project directory:
iverilog -o tb_top_sim tb_top.v top.v src/uart_rx.v src/dma_ctrl.v vvp tb_top_sim
-
View results in GTKWave The testbench generates a VCD waveform file:
dump.vcd
Open it in GTKWave:
gtkwave dump.vcd
uart_rx:- Inputs:
clk,rx - Outputs:
rx_data[7:0],rx_done(pulse)
- Inputs:
dma_ctrl:- Inputs:
clk,rx_done,rx_data[7:0],block_size[7:0] - Outputs:
ram_addr,ram_wdata,ram_we,done_interrupt(pulse)
- Inputs:
top:- Inputs:
clk,rst,rx,block_size[7:0] - Output:
done_interrupt(pulse)
- Inputs:
