diff options
author | mattprost <matt.prost@ni.com> | 2020-03-19 16:09:20 -0500 |
---|---|---|
committer | Aaron Rossetto <aaron.rossetto@ni.com> | 2020-04-02 12:12:26 -0500 |
commit | 6aed4b5a45cf0057bc175e19a86888259bf17cec (patch) | |
tree | d92b825a039345957385d6d0f689dc5f4adb3c91 /host | |
parent | 7ad64600fe3db643ba10a88990263b13dac7d063 (diff) | |
download | uhd-6aed4b5a45cf0057bc175e19a86888259bf17cec.tar.gz uhd-6aed4b5a45cf0057bc175e19a86888259bf17cec.tar.bz2 uhd-6aed4b5a45cf0057bc175e19a86888259bf17cec.zip |
rfnoc: Add multichannel register interface
This utility class implements the register access methods of reg_iface
but adds built-in address translation features for consecutive instances of
an RFNoC block. The register peek and poke methods accept an extra 'instance'
parameter which is used to calculate the absolute address for the register
access. This can be used for accessing registers for the different channels
of a multi-channel block (i.e. Radio, DDC, DUC, etc).
Signed-off-by: mattprost <matt.prost@ni.com>
Diffstat (limited to 'host')
-rw-r--r-- | host/include/uhd/rfnoc/multichan_register_iface.hpp | 294 | ||||
-rw-r--r-- | host/include/uhd/rfnoc/register_iface.hpp | 10 | ||||
-rw-r--r-- | host/tests/CMakeLists.txt | 1 | ||||
-rw-r--r-- | host/tests/multichan_register_iface_test.cpp | 180 |
4 files changed, 480 insertions, 5 deletions
diff --git a/host/include/uhd/rfnoc/multichan_register_iface.hpp b/host/include/uhd/rfnoc/multichan_register_iface.hpp new file mode 100644 index 000000000..9517dde6e --- /dev/null +++ b/host/include/uhd/rfnoc/multichan_register_iface.hpp @@ -0,0 +1,294 @@ +// +// Copyright 2020 Ettus Research, a National Instruments Brand +// +// SPDX-License-Identifier: GPL-3.0-or-later +// + +#ifndef INCLUDED_LIBUHD_RFNOC_MULTICHAN_REGISTER_IFACE_HPP +#define INCLUDED_LIBUHD_RFNOC_MULTICHAN_REGISTER_IFACE_HPP + +#include <uhd/rfnoc/register_iface.hpp> +#include <uhd/rfnoc/register_iface_holder.hpp> +#include <uhd/types/device_addr.hpp> +#include <uhd/types/time_spec.hpp> +#include <cstdint> +#include <functional> +#include <memory> +#include <vector> + +namespace uhd { namespace rfnoc { + +/*! A software interface to access low-level registers in a NoC block, which + * automatically handles address translation between multiple consecutive + * instances of the block. (For instance, accessing registers from multiple + * channels in hardware). + * + * This interface supports the following: + * - Writing and reading registers + * - Hardware timed delays (for time sequencing operations) + * + */ +class multichan_register_iface +{ +public: + using sptr = std::shared_ptr<multichan_register_iface>; + + multichan_register_iface(register_iface_holder& reg_iface_holder, + const uint32_t block_base_addr, + const size_t block_size) + : _reg_iface_holder(reg_iface_holder) + , _block_base_addr(block_base_addr) + , _block_size(block_size) + { + } + + ~multichan_register_iface() = default; + + /*! Write a 32-bit register implemented in the NoC block. + * + * \param addr The byte address of the register to write to (truncated to 20 bits). + * \param data New value of this register. + * \param instance The index of the block of registers to which the write applies + * \param time The time at which the transaction should be executed. + * \param ack Should transaction completion be acknowledged? + * + * \throws op_failed if an ACK is requested and the transaction fails + * \throws op_timeout if an ACK is requested and no response is received + * \throws op_seqerr if an ACK is requested and a sequence error occurs + * \throws op_timeerr if an ACK is requested and a time error occurs (late command) + */ + inline void poke32(uint32_t addr, + uint32_t data, + const size_t instance = 0, + uhd::time_spec_t time = uhd::time_spec_t::ASAP, + bool ack = false) + { + _reg_iface_holder.regs().poke32(_get_addr(addr, instance), data, time, ack); + } + + /*! Write two consecutive 32-bit registers implemented in the NoC block from + * one 64-bit value. + * + * Note: This is a convenience call, because all register pokes are 32-bits. + * This will concatenate two pokes in a block poke, and then return the + * combined result of the two pokes. + * + * \param addr The byte address of the lower 32-bit register to read from + * (truncated to 20 bits). + * \param data New value of the register(s). + * \param instance The index of the block of registers to which the writes apply. + * \param time The time at which the transaction should be executed. + * \param ack Should transaction completion be acknowledged? + * + * \throws op_failed if the transaction fails + * \throws op_timeout if no response is received + * \throws op_seqerr if a sequence error occurs + */ + inline void poke64(uint32_t addr, + uint64_t data, + const size_t instance = 0, + time_spec_t time = uhd::time_spec_t::ASAP, + bool ack = false) + { + _reg_iface_holder.regs().poke64(_get_addr(addr, instance), data, time, ack); + } + + /*! Write multiple 32-bit registers implemented in the NoC block. + * + * This method should be called when multiple writes need to happen that are + * at non-consecutive addresses. For consecutive writes, cf. block_poke32(). + * + * \param addrs The byte addresses of the registers to write to + * (each truncated to 20 bits). + * \param data New values of these registers. The lengths of data and addr + * must match. + * \param instance The index of the block of registers to which the writes apply. + * \param time The time at which the first transaction should be executed. + * \param ack Should transaction completion be acknowledged? + + * \throws uhd::value_error if lengths of data and addr don't match + * \throws op_failed if an ACK is requested and the transaction fails + * \throws op_timeout if an ACK is requested and no response is received + * \throws op_seqerr if an ACK is requested and a sequence error occurs + * \throws op_timeerr if an ACK is requested and a time error occurs (late command) + */ + inline void multi_poke32(const std::vector<uint32_t> addrs, + const std::vector<uint32_t> data, + const size_t instance = 0, + uhd::time_spec_t time = uhd::time_spec_t::ASAP, + bool ack = false) + { + std::vector<uint32_t> abs_addrs(addrs.size()); + std::transform(addrs.begin(), + addrs.end(), + abs_addrs.begin(), + [this, instance]( + uint32_t addr) -> uint32_t { return _get_addr(addr, instance); }); + _reg_iface_holder.regs().multi_poke32(abs_addrs, data, time, ack); + } + + /*! Write multiple consecutive 32-bit registers implemented in the NoC block. + * + * This function will only allow writes to adjacent registers, in increasing + * order. If addr is set to 0, and the length of data is 8, then this method + * triggers eight writes, in order, to addresses 0, 4, 8, 12, 16, 20, 24, 28. + * For arbitrary addresses, cf. multi_poke32(). + * + * Note: There is no guarantee that under the hood, the implementation won't + * separate the writes. + * + * \param first_addr The byte addresses of the first register to write + * \param data New values of these registers + * \param instance The index of the block of registers to which the writes apply. + * \param time The time at which the first transaction should be executed. + * \param ack Should transaction completion be acknowledged? + * + * \throws op_failed if an ACK is requested and the transaction fails + * \throws op_timeout if an ACK is requested and no response is received + * \throws op_seqerr if an ACK is requested and a sequence error occurs + * \throws op_timeerr if an ACK is requested and a time error occurs (late command) + */ + inline void block_poke32(uint32_t first_addr, + const std::vector<uint32_t> data, + const size_t instance = 0, + uhd::time_spec_t time = uhd::time_spec_t::ASAP, + bool ack = false) + { + _reg_iface_holder.regs().block_poke32( + _get_addr(first_addr, instance), data, time, ack); + } + + /*! Read a 32-bit register implemented in the NoC block. + * + * \param addr The byte address of the register to read from (truncated to 20 bits). + * \param instance The index of the block of registers to which the read applies. + * \param time The time at which the transaction should be executed. + * + * \throws op_failed if the transaction fails + * \throws op_timeout if no response is received + * \throws op_seqerr if a sequence error occurs + */ + inline uint32_t peek32(uint32_t addr, + const size_t instance = 0, + time_spec_t time = uhd::time_spec_t::ASAP) + { + return _reg_iface_holder.regs().peek32(_get_addr(addr, instance), time); + } + + /*! Read two consecutive 32-bit registers implemented in the NoC block + * and return them as one 64-bit value. + * + * Note: This is a convenience call, because all register peeks are 32-bits. + * This will concatenate two peeks in a block peek, and then return the + * combined result of the two peeks. + * + * \param addr The byte address of the lower 32-bit register to read from + * (truncated to 20 bits). + * \param instance The index of the block of registers to which the reads apply. + * \param time The time at which the transaction should be executed. + * + * \throws op_failed if the transaction fails + * \throws op_timeout if no response is received + * \throws op_seqerr if a sequence error occurs + */ + inline uint64_t peek64(uint32_t addr, + const size_t instance = 0, + time_spec_t time = uhd::time_spec_t::ASAP) + { + return _reg_iface_holder.regs().peek64(_get_addr(addr, instance), time); + } + + /*! Read multiple 32-bit consecutive registers implemented in the NoC block. + * + * \param first_addr The byte address of the first register to read from + * (truncated to 20 bits). + * \param length The number of 32-bit values to read + * \param instance The index of the block of registers to which the reads apply. + * \param time The time at which the transaction should be executed. + * \return data New value of this register. + * + * Example: If \p first_addr is set to 0, and length is 8, then this + * function will return a vector of length 8, with the content of registers + * at addresses 0, 4, 8, 12, 16, 20, 24, and 28 respectively. + * + * Note: There is no guarantee that under the hood, the implementation won't + * separate the reads. + * + * \throws op_failed if the transaction fails + * \throws op_timeout if no response is received + * \throws op_seqerr if a sequence error occurs + */ + inline std::vector<uint32_t> block_peek32(uint32_t first_addr, + size_t length, + const size_t instance = 0, + time_spec_t time = uhd::time_spec_t::ASAP) + { + return _reg_iface_holder.regs().block_peek32( + _get_addr(first_addr, instance), length, time); + } + + /*! Poll a 32-bit register until its value for all bits in mask match data&mask + * + * This will insert a command into the command queue to wait until a + * register is of a certain value. This can be used, e.g., to poll for a + * lock pin before executing the next command. It is related to sleep(), + * except it has a condition to wait on, rather than an unconditional stall + * duration. The timeout is hardware-timed. + * If the register does not attain the requested value within the requested + * duration, ${something bad happens}. + * + * Example: Assume readback register 16 is a status register, and bit 0 + * indicates a lock is in place (i.e., we want it to be 1) and bit 1 is an + * error flag (i.e., we want it to be 0). The previous command can modify + * the state of the block, so we give it 1ms to settle. In that case, the + * call would be thus: + * + * ~~~{.cpp} + * // iface is a multichan_register_iface::sptr: + * iface->poll32(16, 0x1, 0x3, 1e-3); + * ~~~ + * + * \param addr The byte address of the register to read from (truncated to 20 bits). + * \param data The values that the register must have + * \param mask The bitmask that is applied before checking the readback + * value + * \param timeout The max duration that the register is allowed to take + * before reaching its new state. + * \param instance The index of the block of registers to which the poll applies. + * \param time When the poll should be executed + * \param ack Should transaction completion be acknowledged? This is + * typically only necessary if the software needs a condition to + * be fulfilled before continueing, or during debugging. + * + * \throws op_failed if an ACK is requested and the transaction fails + * \throws op_timeout if an ACK is requested and no response is received + * \throws op_seqerr if an ACK is requested and a sequence error occurs + * \throws op_timeerr if an ACK is requested and a time error occurs (late command) + */ + inline void poll32(uint32_t addr, + uint32_t data, + uint32_t mask, + time_spec_t timeout, + const size_t instance = 0, + time_spec_t time = uhd::time_spec_t::ASAP, + bool ack = false) + { + _reg_iface_holder.regs().poll32( + _get_addr(addr, instance), data, mask, timeout, time, ack); + } + +private: + register_iface_holder& _reg_iface_holder; + uint32_t _block_base_addr; + size_t _block_size; + + inline uint32_t _get_addr(const uint32_t reg_offset, const size_t instance) const + { + return _block_base_addr + reg_offset + _block_size * instance; + } + +}; // class multichan_register_iface + +}} /* namespace uhd::rfnoc */ + +#endif /* INCLUDED_LIBUHD_RFNOC_MULTICHAN_REGISTER_IFACE_HPP */ diff --git a/host/include/uhd/rfnoc/register_iface.hpp b/host/include/uhd/rfnoc/register_iface.hpp index 5d95e6aae..e0849f1c3 100644 --- a/host/include/uhd/rfnoc/register_iface.hpp +++ b/host/include/uhd/rfnoc/register_iface.hpp @@ -127,8 +127,8 @@ public: /*! Write multiple consecutive 32-bit registers implemented in the NoC block. * * This function will only allow writes to adjacent registers, in increasing - * order. If addr is set to 0, and the length of data is 4, then this method - * will trigger four writes, in order, to addresses 0, 4, 8, 12. + * order. If addr is set to 0, and the length of data is 8, then this method + * triggers eight writes, in order, to addresses 0, 4, 8, 12, 16, 20, 24, 28. * For arbitrary addresses, cf. multi_poke32(). * * Note: There is no guarantee that under the hood, the implementation won't @@ -189,9 +189,9 @@ public: * \param time The time at which the transaction should be executed. * \return data New value of this register. * - * Example: If \p first_addr is set to 0, and length is 4, then this - * function will return a vector of length 4, with the content of registers - * at addresses 0, 4, 8, and 12, respectively. + * Example: If \p first_addr is set to 0, and length is 8, then this + * function will return a vector of length 8, with the content of registers + * at addresses 0, 4, 8, 12, 16, 20, 24, and 28 respectively. * * Note: There is no guarantee that under the hood, the implementation won't * separate the reads. diff --git a/host/tests/CMakeLists.txt b/host/tests/CMakeLists.txt index 2742c0385..3665a9d17 100644 --- a/host/tests/CMakeLists.txt +++ b/host/tests/CMakeLists.txt @@ -59,6 +59,7 @@ set(test_sources tx_streamer_test.cpp block_id_test.cpp rfnoc_property_test.cpp + multichan_register_iface_test.cpp ) #turn each test cpp file into an executable with an int main() function diff --git a/host/tests/multichan_register_iface_test.cpp b/host/tests/multichan_register_iface_test.cpp new file mode 100644 index 000000000..3a97d56cd --- /dev/null +++ b/host/tests/multichan_register_iface_test.cpp @@ -0,0 +1,180 @@ +// +// Copyright 2020 Ettus Research, a National Instruments Brand +// +// SPDX-License-Identifier: GPL-3.0-or-later +// + +#include <uhd/rfnoc/mock_block.hpp> +#include <uhd/rfnoc/multichan_register_iface.hpp> +#include <boost/test/unit_test.hpp> +#include <algorithm> +#include <iostream> +#include <vector> + +using namespace uhd::rfnoc; + +namespace { + +constexpr uint32_t BASE_ADDR = 0x8000; +constexpr size_t INSTANCE_SIZE = 0x1000; + +inline uint32_t get_addr_translation(uint32_t offset, size_t instance) +{ + return offset + BASE_ADDR + INSTANCE_SIZE * instance; +} + +} // namespace + +BOOST_AUTO_TEST_CASE(test_poke32) +{ + auto mock_reg_iface = std::make_shared<mock_reg_iface_t>(); + register_iface_holder mock_holder{mock_reg_iface}; + multichan_register_iface block_reg_iface{mock_holder, BASE_ADDR, INSTANCE_SIZE}; + uint32_t addr = 0x100; + uint32_t data = 0x1230; + block_reg_iface.poke32(addr, data); + uint32_t abs_addr = get_addr_translation(addr, 0); + BOOST_CHECK_EQUAL(mock_reg_iface->write_memory[abs_addr], data); + for (size_t instance = 0; instance < 4; instance++) { + data = 0xabc0 | instance; + block_reg_iface.poke32(addr, data, instance); + abs_addr = get_addr_translation(addr, instance); + BOOST_CHECK_EQUAL(mock_reg_iface->write_memory[abs_addr], data); + } +} + +BOOST_AUTO_TEST_CASE(test_peek32) +{ + auto mock_reg_iface = std::make_shared<mock_reg_iface_t>(); + register_iface_holder mock_holder{mock_reg_iface}; + multichan_register_iface block_reg_iface{mock_holder, BASE_ADDR, INSTANCE_SIZE}; + uint32_t addr = 0x200; + for (size_t instance = 0; instance < 4; instance++) { + uint32_t data = 0xdef0 | instance; + uint32_t abs_addr = get_addr_translation(addr, instance); + mock_reg_iface->read_memory[abs_addr] = data; + if (instance == 0) { + BOOST_CHECK_EQUAL(block_reg_iface.peek32(addr), data); + } + BOOST_CHECK_EQUAL(block_reg_iface.peek32(addr, instance), data); + } +} + +BOOST_AUTO_TEST_CASE(test_multi_poke32) +{ + auto mock_reg_iface = std::make_shared<mock_reg_iface_t>(); + register_iface_holder mock_holder{mock_reg_iface}; + multichan_register_iface block_reg_iface{mock_holder, BASE_ADDR, INSTANCE_SIZE}; + std::vector<uint32_t> addrs = {0, 4, 8, 12, 16, 20, 24, 28}; + std::vector<uint32_t> data = { + 0x0000, 0x0010, 0x0200, 0x3000, 0x0004, 0x0050, 0x0600, 0x7000}; + block_reg_iface.multi_poke32(addrs, data); + for (size_t i = 0; i < addrs.size(); i++) { + uint32_t abs_addr = get_addr_translation(addrs[i], 0); + BOOST_CHECK_EQUAL(mock_reg_iface->write_memory[abs_addr], data[i]); + } + std::reverse(data.begin(), data.end()); + for (size_t instance = 0; instance < 4; instance++) { + block_reg_iface.multi_poke32(addrs, data, instance); + for (size_t i = 0; i < addrs.size(); i++) { + uint32_t abs_addr = get_addr_translation(addrs[i], instance); + BOOST_CHECK_EQUAL(mock_reg_iface->write_memory[abs_addr], data[i]); + } + } +} + +BOOST_AUTO_TEST_CASE(test_block_poke32) +{ + auto mock_reg_iface = std::make_shared<mock_reg_iface_t>(); + register_iface_holder mock_holder{mock_reg_iface}; + multichan_register_iface block_reg_iface{mock_holder, BASE_ADDR, INSTANCE_SIZE}; + uint32_t addr = 0x100; + std::vector<uint32_t> data = { + 0x0000, 0x0010, 0x0200, 0x3000, 0x0004, 0x0050, 0x0600, 0x7000}; + block_reg_iface.block_poke32(addr, data); + for (size_t i = 0; i < data.size(); i++) { + uint32_t abs_addr = get_addr_translation(addr + i * sizeof(uint32_t), 0); + BOOST_CHECK_EQUAL(mock_reg_iface->write_memory[abs_addr], data[i]); + } + std::reverse(data.begin(), data.end()); + for (size_t instance = 0; instance < 4; instance++) { + block_reg_iface.block_poke32(addr, data, instance); + for (size_t i = 0; i < data.size(); i++) { + uint32_t abs_addr = + get_addr_translation(addr + i * sizeof(uint32_t), instance); + BOOST_CHECK_EQUAL(mock_reg_iface->write_memory[abs_addr], data[i]); + } + } +} + +BOOST_AUTO_TEST_CASE(test_block_peek32) +{ + auto mock_reg_iface = std::make_shared<mock_reg_iface_t>(); + register_iface_holder mock_holder{mock_reg_iface}; + multichan_register_iface block_reg_iface{mock_holder, BASE_ADDR, INSTANCE_SIZE}; + uint32_t addr = 0x200; + std::vector<uint32_t> data = { + 0x0008, 0x0090, 0x0a00, 0xb000, 0x000c, 0x00d0, 0x0e00, 0xf000}; + for (size_t instance = 0; instance < 4; instance++) { + for (size_t i = 0; i < data.size(); i++) { + uint32_t abs_addr = + get_addr_translation(addr + i * sizeof(uint32_t), instance); + mock_reg_iface->read_memory[abs_addr] = data[i]; + } + std::vector<uint32_t> peek_data = + block_reg_iface.block_peek32(addr, data.size(), instance); + BOOST_CHECK_EQUAL(peek_data.size(), data.size()); + for (size_t i = 0; i < data.size(); i++) { + BOOST_CHECK_EQUAL(peek_data[i], data[i]); + } + if (instance == 0) { + peek_data = block_reg_iface.block_peek32(addr, data.size()); + BOOST_CHECK_EQUAL(peek_data.size(), data.size()); + for (size_t i = 0; i < data.size(); i++) { + BOOST_CHECK_EQUAL(peek_data[i], data[i]); + } + } + } +} + +BOOST_AUTO_TEST_CASE(test_poke64) +{ + auto mock_reg_iface = std::make_shared<mock_reg_iface_t>(); + register_iface_holder mock_holder{mock_reg_iface}; + multichan_register_iface block_reg_iface{mock_holder, BASE_ADDR, INSTANCE_SIZE}; + uint32_t addr = 0x100; + uint64_t data = 0xabcdef12; + block_reg_iface.poke64(addr, data); + uint32_t abs_addr = get_addr_translation(addr, 0); + BOOST_CHECK_EQUAL( + mock_reg_iface->write_memory[abs_addr], uint32_t(data & 0xFFFFFFFF)); + BOOST_CHECK_EQUAL( + mock_reg_iface->write_memory[abs_addr + 4], uint32_t((data >> 32) & 0xFFFFFFFF)); + for (size_t instance = 0; instance < 4; instance++) { + data = 0x12345670 | instance; + block_reg_iface.poke64(addr, data, instance); + abs_addr = get_addr_translation(addr, instance); + BOOST_CHECK_EQUAL( + mock_reg_iface->write_memory[abs_addr], uint32_t(data & 0xFFFFFFFF)); + BOOST_CHECK_EQUAL(mock_reg_iface->write_memory[abs_addr + 4], + uint32_t((data >> 32) & 0xFFFFFFFF)); + } +} + +BOOST_AUTO_TEST_CASE(test_peek64) +{ + auto mock_reg_iface = std::make_shared<mock_reg_iface_t>(); + register_iface_holder mock_holder{mock_reg_iface}; + multichan_register_iface block_reg_iface{mock_holder, BASE_ADDR, INSTANCE_SIZE}; + uint32_t addr = 0x200; + for (size_t instance = 0; instance < 4; instance++) { + uint64_t data = 0x9abcdef0 | instance; + uint32_t abs_addr = get_addr_translation(addr, instance); + mock_reg_iface->read_memory[abs_addr] = uint32_t(data & 0xFFFFFFFF); + mock_reg_iface->read_memory[abs_addr + 4] = uint32_t((data >> 32) & 0xFFFFFFFF); + if (instance == 0) { + BOOST_CHECK_EQUAL(block_reg_iface.peek64(addr), data); + } + BOOST_CHECK_EQUAL(block_reg_iface.peek64(addr, instance), data); + } +} |