403 lines
14 KiB
VHDL
403 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_empty: 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;
|
|
in_empty <= not s_fifo_valid;
|
|
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.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' 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;
|