-- -- 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;