redpitaya-puzzlefw/fpga/rtl/dma_write_channel.vhd

402 lines
14 KiB
VHDL

--
-- Management of DMA transfers from FPGA to memory.
--
-- Joris van Rantwijk 2024
--
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
use work.puzzlefw_pkg.all;
entity dma_write_channel is
generic (
-- Preferred DMA burst length as the 2-log of the number of beats.
-- Transactions will use the preferred burst length unless there
-- are fewer words available in the buffer.
transfer_size_bits: integer range 0 to 4 := 4;
-- Size of the input data queue as 2-log of the number of words.
queue_size_bits: integer range 5 to 16 := 10;
-- Number of clock cycles to wait before starting a small DMA burst.
idle_timeout: natural := 256
);
port (
-- Main clock, active on rising edge.
clk: in std_logic;
-- Reset, active high, synchronous to main clock.
reset: in std_logic;
-- High to enable the channel, low to pause the channel.
-- When channel_en is low, any ongoing transfer will be completed but
-- no new transfer will be started.
channel_en: in std_logic;
-- High if there uncompleted DMA transfers are in progress.
-- If a DMA error occurs, the channel may get stuck in a busy state.
channel_busy: out std_logic;
-- High to initialize the channel.
--
-- This resets the write pointer to the start of the buffer and deletes
-- all data from the queue.
--
-- This function must be used whenever the buffer address range is changed.
-- This function must not be used while uncompleted DMA transfers
-- are in progress, except to recover from an error.
channel_init: in std_logic;
-- Start and end address of the DMA buffer.
-- Both addresses are relative to the DMA window base address.
-- The write pointer wraps to the start address when it would
-- have reached the end address.
addr_start: in std_logic_vector(31 downto 7);
addr_end: in std_logic_vector(31 downto 7);
-- When the write pointer reaches the limit address, DMA transfers
-- are paused until software updates the limit.
addr_limit: in std_logic_vector(31 downto 7);
-- An interrupt is triggered when the write pointer equals the
-- interrupt address.
addr_interrupt: in std_logic_vector(31 downto 3);
-- Write pointer.
addr_pointer: out std_logic_vector(31 downto 3);
-- High to enable interrupt on reaching a configured address.
intr_en: in std_logic;
-- Pulsed high to clear a previous interrupt condition.
intr_clear: in std_logic;
-- High if an enabled interrupt condition occurred and the interrupt
-- has not been cleared yet.
intr_out: out std_logic;
-- Input data stream to the channel.
in_valid: in std_logic;
in_ready: out std_logic;
in_data: in dma_data_type;
-- Signals to AXI master.
write_cmd_addr: out dma_address_type;
write_cmd_length: out dma_burst_length_type;
write_cmd_valid: out std_logic;
write_cmd_ready: in std_logic;
write_data: out dma_data_type;
write_data_ready: in std_logic;
write_finished: in std_logic
);
end entity;
architecture arch of dma_write_channel is
constant transfer_size: integer range 1 to 16 := 2**transfer_size_bits;
type state_type is (STATE_SINGLE_IDLE, STATE_SINGLE_START, STATE_SINGLE_DATA,
STATE_BURST_PREPARE,
STATE_BURST_IDLE, STATE_BURST_START, STATE_BURST_DATA);
type regs_type is record
cmd_valid: std_logic;
cmd_addr: std_logic_vector(31 downto 3);
cmd_full_burst: std_logic;
addr_pointer: std_logic_vector(31 downto 3);
intr_out: std_logic;
channel_busy: std_logic;
state: state_type;
pending_beats: unsigned(3 downto 0);
pending_transfers: unsigned(3 downto 0);
idle_timer: integer range 0 to idle_timeout - 1;
end record;
constant regs_init: regs_type := (
cmd_valid => '0',
cmd_addr => (others => '0'),
cmd_full_burst => '0',
addr_pointer => (others => '0'),
intr_out => '0',
channel_busy => '0',
state => STATE_SINGLE_IDLE,
pending_beats => (others => '0'),
pending_transfers => (others => '0'),
idle_timer => 0
);
signal r: regs_type := regs_init;
signal rnext: regs_type;
signal s_fifo_reset: std_logic;
signal s_fifo_valid: std_logic;
signal s_fifo_length: unsigned(queue_size_bits - 1 downto 0);
-- Return true if address is aligned to a full-size burst.
function is_address_aligned(addr: std_logic_vector(31 downto 3))
return boolean
is begin
if transfer_size = 1 then
return true;
else
return (unsigned(addr(transfer_size_bits + 2 downto 3)) = 0);
end if;
end function;
begin
--
-- Data FIFO.
--
inst_fifo: entity work.simple_fifo
generic map (
data_width => 64,
fifo_depth_bits => queue_size_bits )
port map (
clk => clk,
reset => s_fifo_reset,
in_valid => in_valid,
in_ready => in_ready,
in_data => in_data,
out_valid => s_fifo_valid,
out_ready => write_data_ready,
out_data => write_data,
queue_length => s_fifo_length );
-- Drive output ports.
channel_busy <= r.channel_busy;
addr_pointer <= r.addr_pointer;
intr_out <= r.intr_out;
write_cmd_addr <= r.cmd_addr;
write_cmd_length <= "0000" when (r.cmd_full_burst = '0') else
std_logic_vector(to_unsigned(transfer_size - 1, 4));
write_cmd_valid <= r.cmd_valid;
-- Drive reset signal to FIFO.
s_fifo_reset <= reset or channel_init;
--
-- Combinatorial process.
--
process (all) is
variable v: regs_type;
begin
-- Load current register values.
v := r;
-- State machine.
case r.state is
when STATE_SINGLE_IDLE =>
v.cmd_full_burst := '0';
-- Clear busy flag.
if r.pending_transfers = 0 then
v.channel_busy := '0';
end if;
-- Update idle timer.
if (s_fifo_valid = '1') and (r.idle_timer /= idle_timeout - 1) then
v.idle_timer := r.idle_timer + 1;
end if;
-- Switch to burst mode if the FIFO has enough data,
-- and the address is aligned to a full burst.
if (transfer_size > 1)
and (s_fifo_length >= transfer_size)
and is_address_aligned(r.cmd_addr) then
v.state := STATE_BURST_PREPARE;
-- Otherwise, start a single-beat transfer when possible.
elsif (channel_en = '1')
and (s_fifo_valid = '1')
and (r.cmd_addr(31 downto 7) /= addr_limit)
and (r.pending_transfers /= 15)
and ((transfer_size = 1)
or (not is_address_aligned(r.cmd_addr))
or (r.idle_timer = idle_timeout - 1)) then
v.cmd_valid := '1';
v.channel_busy := '1';
v.pending_beats := to_unsigned(0, v.pending_beats'length);
v.state := STATE_SINGLE_START;
end if;
when STATE_SINGLE_START =>
-- Wait until start of transfer.
if write_cmd_ready = '1' then
v.cmd_valid := '0';
-- Update pending transfers.
v.pending_transfers := r.pending_transfers + 1;
-- Update command address.
v.cmd_addr := std_logic_vector(unsigned(r.cmd_addr) + 1);
if v.cmd_addr(31 downto 7) = addr_end then
-- Wrap at end of buffer.
v.cmd_addr := addr_start & "0000";
end if;
if write_data_ready = '1' then
v.state := STATE_SINGLE_IDLE;
else
v.state := STATE_SINGLE_DATA;
end if;
end if;
-- Reset idle counter.
v.idle_timer := 0;
when STATE_SINGLE_DATA =>
-- Wait until data accepted.
if (write_data_ready = '1') and (r.pending_beats = 0) then
v.state := STATE_SINGLE_IDLE;
end if;
when STATE_BURST_PREPARE =>
-- Wait until pending transfers are completed.
if r.pending_transfers = 0 then
v.state := STATE_BURST_IDLE;
end if;
-- Reset idle counter.
v.idle_timer := 0;
when STATE_BURST_IDLE =>
v.cmd_full_burst := '1';
-- Clear busy flag.
if r.pending_transfers = 0 then
v.channel_busy := '0';
end if;
-- Switch to single transfer mode if FIFO has insufficient data.
if (s_fifo_length < transfer_size)
and (r.pending_transfers = 0) then
v.state := STATE_SINGLE_IDLE;
end if;
-- Start a burst transfer if possible.
if (channel_en = '1')
and (s_fifo_length >= transfer_size)
and (r.cmd_addr(31 downto 7) /= addr_limit)
and (r.pending_transfers /= 15) then
v.cmd_valid := '1';
v.channel_busy := '1';
v.pending_beats := to_unsigned(transfer_size - 1, v.pending_beats'length);
v.state := STATE_BURST_START;
end if;
when STATE_BURST_START =>
-- Wait until start of transfer.
if write_cmd_ready = '1' then
v.cmd_valid := '0';
-- Update pending transfers.
v.pending_transfers := r.pending_transfers + 1;
-- Update command address.
v.cmd_addr := std_logic_vector(unsigned(r.cmd_addr) + transfer_size);
if v.cmd_addr(31 downto 7) = addr_end then
-- Wrap at end of buffer.
v.cmd_addr := addr_start & "0000";
end if;
v.state := STATE_BURST_DATA;
end if;
when STATE_BURST_DATA =>
-- Wait until last beat.
if (write_data_ready = '1') and (r.pending_beats = 0) then
v.state := STATE_BURST_IDLE;
end if;
end case;
-- Count write beats to find end of transfer.
if write_data_ready = '1' then
v.pending_beats := r.pending_beats - 1;
end if;
-- Update pointer on write completion.
if (write_finished = '1') and (r.pending_transfers /= 0) then
v.pending_transfers := v.pending_transfers - 1;
if r.cmd_full_burst = '1' then
-- Completion of a full burst.
v.addr_pointer := std_logic_vector(unsigned(r.addr_pointer) + transfer_size);
-- Raise interrupt when pointer passes threshold.
if (intr_en = '1')
and (r.addr_pointer(31 downto 3 + transfer_size_bits)
= addr_interrupt(31 downto 3 + transfer_size_bits)) then
v.intr_out := '1';
end if;
else
-- Completion of single transfer.
v.addr_pointer := std_logic_vector(unsigned(r.addr_pointer) + 1);
end if;
-- Wrap at end of buffer.
if v.addr_pointer(31 downto 7) = addr_end then
v.addr_pointer := addr_start & "0000";
end if;
end if;
-- Raise interrupt when pointer equals threshold.
if (intr_en = '1') and (r.addr_pointer = addr_interrupt) then
v.intr_out := '1';
end if;
-- Clear interrupt.
if intr_clear = '1' then
v.intr_out := '0';
end if;
-- Initialize pointers.
if channel_init = '1' then
v.cmd_addr := addr_start & "0000";
v.addr_pointer := addr_start & "0000";
v.state := STATE_SINGLE_IDLE;
v.pending_transfers := (others => '0');
v.idle_timer := 0;
end if;
-- Synchronous reset.
if reset = '1' then
v := regs_init;
end if;
-- Drive new register values to synchronous process.
rnext <= v;
end process;
--
-- Synchronous process.
--
process (clk) is
begin
if rising_edge(clk) then
r <= rnext;
end if;
end process;
end architecture;