-- ################################################################################################# -- # << NEORV32 - Direct Memory Access (DMA) Controller >> # -- # ********************************************************************************************* # -- # Simple single-channel scatter/gather DMA controller that is also capable of transforming data # -- # while moving it from source to destination. # -- # ********************************************************************************************* # -- # BSD 3-Clause License # -- # # -- # The NEORV32 RISC-V Processor, https://github.com/stnolting/neorv32 # -- # Copyright (c) 2024, Stephan Nolting. All rights reserved. # -- # # -- # Redistribution and use in source and binary forms, with or without modification, are # -- # permitted provided that the following conditions are met: # -- # # -- # 1. Redistributions of source code must retain the above copyright notice, this list of # -- # conditions and the following disclaimer. # -- # # -- # 2. Redistributions in binary form must reproduce the above copyright notice, this list of # -- # conditions and the following disclaimer in the documentation and/or other materials # -- # provided with the distribution. # -- # # -- # 3. Neither the name of the copyright holder nor the names of its contributors may be used to # -- # endorse or promote products derived from this software without specific prior written # -- # permission. # -- # # -- # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS # -- # OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF # -- # MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE # -- # COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, # -- # EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE # -- # GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED # -- # AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING # -- # NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED # -- # OF THE POSSIBILITY OF SUCH DAMAGE. # -- ################################################################################################# library ieee; use ieee.std_logic_1164.all; use ieee.numeric_std.all; library neorv32; use neorv32.neorv32_package.all; entity neorv32_dma is port ( clk_i : in std_ulogic; -- global clock line rstn_i : in std_ulogic; -- global reset line, low-active, async bus_req_i : in bus_req_t; -- bus request bus_rsp_o : out bus_rsp_t; -- bus response dma_req_o : out bus_req_t; -- DMA request dma_rsp_i : in bus_rsp_t; -- DMA response firq_i : in std_ulogic_vector(15 downto 0); -- CPU FIRQ channels irq_o : out std_ulogic -- transfer done interrupt ); end neorv32_dma; architecture neorv32_dma_rtl of neorv32_dma is -- transfer type register bits -- constant type_num_lo_c : natural := 0; -- r/w: Number of elements to transfer, LSB constant type_num_hi_c : natural := 23; -- r/w: Number of elements to transfer, MSB -- constant type_qsel_lo_c : natural := 27; -- r/w: Data quantity select, LSB, see below constant type_qsel_hi_c : natural := 28; -- r/w: Data quantity select, MSB, see below constant type_src_inc_c : natural := 29; -- r/w: SRC constant (0) or incrementing (1) address constant type_dst_inc_c : natural := 30; -- r/w: DST constant (0) or incrementing (1) address constant type_endian_c : natural := 31; -- r/w: Convert Endianness when set -- control and status register bits -- constant ctrl_en_c : natural := 0; -- r/w: DMA enable constant ctrl_auto_c : natural := 1; -- r/w: enable FIRQ-triggered transfer constant ctrl_fence_c : natural := 3; -- r/w: issue FENCE operation when DMA is done -- constant ctrl_error_rd_c : natural := 8; -- r/-: error during read transfer constant ctrl_error_wr_c : natural := 9; -- r/-: error during write transfer constant ctrl_busy_c : natural := 10; -- r/-: DMA transfer in progress constant ctrl_done_c : natural := 11; -- r/c: a DMA transfer was executed/attempted -- constant ctrl_firq_mask_lsb_c : natural := 16; -- r/w: FIRQ trigger mask LSB constant ctrl_firq_mask_msb_c : natural := 31; -- r/w: FIRQ trigger mask MSB -- transfer quantities -- constant qsel_b2b_c : std_ulogic_vector(1 downto 0) := "00"; -- byte to byte constant qsel_b2uw_c : std_ulogic_vector(1 downto 0) := "01"; -- byte to unsigned word constant qsel_b2sw_c : std_ulogic_vector(1 downto 0) := "10"; -- byte to signed word constant qsel_w2w_c : std_ulogic_vector(1 downto 0) := "11"; -- word to word -- configuration registers -- type config_t is record enable : std_ulogic; -- DMA enabled when set auto : std_ulogic; -- FIRQ-driven auto transfer fence : std_ulogic; -- issue FENCE operation when DMA is done firq_mask : std_ulogic_vector(15 downto 0); -- FIRQ trigger mask src_base : std_ulogic_vector(31 downto 0); -- source base address dst_base : std_ulogic_vector(31 downto 0); -- destination base address num : std_ulogic_vector(23 downto 0); -- number of elements qsel : std_ulogic_vector(01 downto 0); -- data quantity select src_inc : std_ulogic; -- constant (0) or incrementing (1) source address dst_inc : std_ulogic; -- constant (0) or incrementing (1) destination address endian : std_ulogic; -- convert endianness when set start : std_ulogic; -- transfer start trigger done : std_ulogic; -- transfer was executed (but might have failed) end record; signal config : config_t; -- bus access engine -- type state_t is (S_IDLE, S_READ, S_WRITE, S_NEXT); type engine_t is record state : state_t; src_addr : std_ulogic_vector(31 downto 0); dst_addr : std_ulogic_vector(31 downto 0); num : std_ulogic_vector(23 downto 0); err_rd : std_ulogic; err_wr : std_ulogic; src_add : unsigned(31 downto 0); dst_add : unsigned(31 downto 0); busy : std_ulogic; done : std_ulogic; end record; signal engine : engine_t; -- data alignment -- signal align_buf : std_ulogic_vector(31 downto 0); signal align_end : std_ulogic_vector(31 downto 0); -- FIRQ trigger -- signal firq_buf : std_ulogic_vector(15 downto 0); signal match : std_ulogic; signal match_ff : std_ulogic; signal atrigger : std_ulogic; begin -- Bus Access ----------------------------------------------------------------------------- -- ------------------------------------------------------------------------------------------- bus_access: process(rstn_i, clk_i) begin if (rstn_i = '0') then bus_rsp_o.ack <= '0'; bus_rsp_o.err <= '0'; bus_rsp_o.data <= (others => '0'); config.enable <= '0'; config.auto <= '0'; config.fence <= '0'; config.firq_mask <= (others => '0'); config.src_base <= (others => '0'); config.dst_base <= (others => '0'); config.num <= (others => '0'); config.qsel <= (others => '0'); config.src_inc <= '0'; config.dst_inc <= '0'; config.endian <= '0'; config.start <= '0'; config.done <= '0'; elsif rising_edge(clk_i) then -- bus handshake -- bus_rsp_o.ack <= bus_req_i.stb; bus_rsp_o.err <= '0'; bus_rsp_o.data <= (others => '0'); -- defaults -- config.start <= '0'; -- default config.done <= config.enable and (config.done or engine.done); -- set if enabled and transfer done if (bus_req_i.stb = '1') then -- write access -- if (bus_req_i.rw = '1') then if (bus_req_i.addr(3 downto 2) = "00") then -- control and status register config.enable <= bus_req_i.data(ctrl_en_c); config.auto <= bus_req_i.data(ctrl_auto_c); config.fence <= bus_req_i.data(ctrl_fence_c); config.done <= '0'; -- clear on write access config.firq_mask <= bus_req_i.data(ctrl_firq_mask_msb_c downto ctrl_firq_mask_lsb_c); end if; if (bus_req_i.addr(3 downto 2) = "01") then -- source base address config.src_base <= bus_req_i.data; end if; if (bus_req_i.addr(3 downto 2) = "10") then -- destination base address config.dst_base <= bus_req_i.data; end if; if (bus_req_i.addr(3 downto 2) = "11") then -- transfer type register config.num <= bus_req_i.data(type_num_hi_c downto type_num_lo_c); config.qsel <= bus_req_i.data(type_qsel_hi_c downto type_qsel_lo_c); config.src_inc <= bus_req_i.data(type_src_inc_c); config.dst_inc <= bus_req_i.data(type_dst_inc_c); config.endian <= bus_req_i.data(type_endian_c); config.start <= '1'; -- trigger DMA operation end if; -- read access -- else case bus_req_i.addr(3 downto 2) is when "00" => -- control and status register bus_rsp_o.data(ctrl_en_c) <= config.enable; bus_rsp_o.data(ctrl_auto_c) <= config.auto; bus_rsp_o.data(ctrl_fence_c) <= config.fence; bus_rsp_o.data(ctrl_error_rd_c) <= engine.err_rd; bus_rsp_o.data(ctrl_error_wr_c) <= engine.err_wr; bus_rsp_o.data(ctrl_busy_c) <= engine.busy; bus_rsp_o.data(ctrl_done_c) <= config.done; bus_rsp_o.data(ctrl_firq_mask_msb_c downto ctrl_firq_mask_lsb_c) <= config.firq_mask; when "01" => -- address of last read access bus_rsp_o.data <= engine.src_addr; when "10" => -- address of last write access bus_rsp_o.data <= engine.dst_addr; when others => -- transfer type register bus_rsp_o.data(type_num_hi_c downto type_num_lo_c) <= engine.num; bus_rsp_o.data(type_qsel_hi_c downto type_qsel_lo_c) <= config.qsel; bus_rsp_o.data(type_src_inc_c) <= config.src_inc; bus_rsp_o.data(type_dst_inc_c) <= config.dst_inc; bus_rsp_o.data(type_endian_c) <= config.endian; end case; end if; end if; end if; end process bus_access; -- Automatic Trigger ---------------------------------------------------------------------- -- ------------------------------------------------------------------------------------------- automatic_trigger: process(rstn_i, clk_i) begin if (rstn_i = '0') then firq_buf <= (others => '0'); match_ff <= '0'; atrigger <= '0'; elsif rising_edge(clk_i) then firq_buf <= firq_i; match_ff <= match; atrigger <= match and (not match_ff); -- trigger on rising edge of FIRQ end if; end process automatic_trigger; -- logical OR of all enabled trigger FIRQs -- match <= or_reduce_f(firq_buf and config.firq_mask); -- Bus Access Engine ---------------------------------------------------------------------- -- ------------------------------------------------------------------------------------------- bus_engine: process(rstn_i, clk_i) begin if (rstn_i = '0') then engine.state <= S_IDLE; engine.src_addr <= (others => '0'); engine.dst_addr <= (others => '0'); engine.num <= (others => '0'); engine.err_rd <= '0'; engine.err_wr <= '0'; engine.done <= '0'; dma_req_o.rw <= '0'; dma_req_o.stb <= '0'; elsif rising_edge(clk_i) then -- defaults -- engine.done <= '0'; dma_req_o.stb <= '0'; -- state machine -- case engine.state is when S_IDLE => -- idle, waiting for start trigger -- ------------------------------------------------------------ engine.src_addr <= config.src_base; engine.dst_addr <= config.dst_base; engine.num <= config.num; if (config.enable = '1') and (((config.auto = '0') and (config.start = '1')) or -- manual trigger ((config.auto = '1') and (atrigger = '1'))) then -- automatic trigger engine.err_rd <= '0'; engine.err_wr <= '0'; dma_req_o.rw <= '0'; -- read dma_req_o.stb <= '1'; -- issue read request engine.state <= S_READ; end if; when S_READ => -- pending read access -- ------------------------------------------------------------ if (dma_rsp_i.err = '1') then engine.done <= '1'; engine.err_rd <= '1'; engine.state <= S_IDLE; elsif (dma_rsp_i.ack = '1') then dma_req_o.rw <= '1'; -- write dma_req_o.stb <= '1'; -- issue write request engine.state <= S_WRITE; end if; when S_WRITE => -- pending write access -- ------------------------------------------------------------ if (dma_rsp_i.err = '1') then engine.done <= '1'; engine.err_wr <= '1'; engine.state <= S_IDLE; elsif (dma_rsp_i.ack = '1') then engine.num <= std_ulogic_vector(unsigned(engine.num) - 1); engine.state <= S_NEXT; end if; when S_NEXT => -- check if done; prepare next access -- ------------------------------------------------------------ if (or_reduce_f(engine.num) = '0') or (config.enable = '0') then -- transfer done or aborted? engine.done <= '1'; engine.state <= S_IDLE; else if (config.src_inc = '1') then -- incrementing source address engine.src_addr <= std_ulogic_vector(unsigned(engine.src_addr) + engine.src_add); end if; if (config.dst_inc = '1') then -- incrementing destination address engine.dst_addr <= std_ulogic_vector(unsigned(engine.dst_addr) + engine.dst_add); end if; dma_req_o.rw <= '0'; -- read dma_req_o.stb <= '1'; -- issue read request engine.state <= S_READ; end if; when others => -- undefined -- ------------------------------------------------------------ engine.state <= S_IDLE; end case; end if; end process bus_engine; -- transfer in progress? -- engine.busy <= '0' when (engine.state = S_IDLE) else '1'; -- transfer-done interrupt -- irq_o <= engine.done and config.enable; -- no interrupt if transfer was aborted -- bus output -- dma_req_o.priv <= priv_mode_m_c; -- privileged access dma_req_o.src <= '0'; -- source = data access dma_req_o.addr <= engine.src_addr when (engine.state = S_READ) else engine.dst_addr; dma_req_o.rvso <= '0'; -- no reservation set operation possible dma_req_o.fence <= config.enable and config.fence and engine.done; -- issue FENCE operation when transfer is done -- address increment -- address_inc: process(config.qsel) begin case config.qsel is when qsel_b2b_c => engine.src_add <= to_unsigned(1, 32); engine.dst_add <= to_unsigned(1, 32); -- byte -> byte when qsel_w2w_c => engine.src_add <= to_unsigned(4, 32); engine.dst_add <= to_unsigned(4, 32); -- word -> word when others => engine.src_add <= to_unsigned(1, 32); engine.dst_add <= to_unsigned(4, 32); -- byte -> word end case; end process address_inc; -- Data Transformer ----------------------------------------------------------------------- -- ------------------------------------------------------------------------------------------- -- endianness conversion -- align_end <= dma_rsp_i.data when (config.endian = '0') else bswap32_f(dma_rsp_i.data); -- source data alignment -- src_align: process(rstn_i, clk_i) begin if (rstn_i = '0') then align_buf <= (others => '0'); elsif rising_edge(clk_i) then if (engine.state = S_READ) then if (config.qsel = qsel_w2w_c) then -- word align_buf <= align_end; else -- byte case engine.src_addr(1 downto 0) is when "00" => -- byte 0 align_buf(07 downto 0) <= align_end(07 downto 00); align_buf(31 downto 8) <= (others => (config.qsel(1) and align_end(07))); -- sign extension when "01" => -- byte 1 align_buf(07 downto 0) <= align_end(15 downto 08); align_buf(31 downto 8) <= (others => (config.qsel(1) and align_end(15))); -- sign extension when "10" => -- byte 2 align_buf(07 downto 0) <= align_end(23 downto 16); align_buf(31 downto 8) <= (others => (config.qsel(1) and align_end(23))); -- sign extension when others => -- byte 3 align_buf(07 downto 0) <= align_end(31 downto 24); align_buf(31 downto 8) <= (others => (config.qsel(1) and align_end(31))); -- sign extension end case; end if; end if; end if; end process src_align; -- destination data alignment -- dst_align: process(config.qsel, align_buf, engine.dst_addr) begin dma_req_o.ben <= (others => '0'); -- default if (config.qsel = qsel_b2b_c) then -- byte dma_req_o.data(07 downto 00) <= align_buf(7 downto 0); dma_req_o.data(15 downto 08) <= align_buf(7 downto 0); dma_req_o.data(23 downto 16) <= align_buf(7 downto 0); dma_req_o.data(31 downto 24) <= align_buf(7 downto 0); dma_req_o.ben(to_integer(unsigned(engine.dst_addr(1 downto 0)))) <= '1'; else -- word dma_req_o.data <= align_buf; dma_req_o.ben <= "1111"; end if; end process dst_align; end neorv32_dma_rtl;