From 674229791f43d4c276a1bc7f2f7c1c9e356159f1 Mon Sep 17 00:00:00 2001 From: Joris van Rantwijk Date: Wed, 18 Sep 2024 19:48:34 +0200 Subject: [PATCH] Add C++ software Not tested yet. --- os/src/userspace/Makefile | 12 +- os/src/userspace/data_server.hpp | 522 ++++++++++++++ os/src/userspace/interrupt_manager.hpp | 114 ++++ os/src/userspace/logging.hpp | 59 ++ os/src/userspace/puzzlecmd.cpp | 599 ++++++++++++++++ os/src/userspace/puzzlefw.cpp | 171 +++++ os/src/userspace/puzzlefw.hpp | 899 +++++++++++++++++++++++++ 7 files changed, 2374 insertions(+), 2 deletions(-) create mode 100644 os/src/userspace/data_server.hpp create mode 100644 os/src/userspace/interrupt_manager.hpp create mode 100644 os/src/userspace/logging.hpp create mode 100644 os/src/userspace/puzzlecmd.cpp create mode 100644 os/src/userspace/puzzlefw.cpp create mode 100644 os/src/userspace/puzzlefw.hpp diff --git a/os/src/userspace/Makefile b/os/src/userspace/Makefile index 7db6d73..123976d 100644 --- a/os/src/userspace/Makefile +++ b/os/src/userspace/Makefile @@ -3,14 +3,22 @@ # CC = $(CROSS_COMPILE)gcc +CXX = $(CROSS_COMPILE)g++ CFLAGS = -Wall -O2 +CXXFLAGS = -std=c++17 -Wall -O2 .PHONY: all -all: testje +all: puzzlecmd + +puzzlecmd: puzzlecmd.o puzzlefw.o + $(CXX) -o $@ $(LDFLAGS) $^ + +puzzlecmd.o: puzzlecmd.cpp logging.hpp puzzlefw.hpp interrupt_manager.hpp data_server.hpp +puzzlefw.o: puzzlefw.cpp puzzlefw.hpp testje: testje.c .PHONY: clean clean: - $(RM) -f testje + $(RM) -f -- puzzlecmd testje *.o diff --git a/os/src/userspace/data_server.hpp b/os/src/userspace/data_server.hpp new file mode 100644 index 0000000..956a513 --- /dev/null +++ b/os/src/userspace/data_server.hpp @@ -0,0 +1,522 @@ +/* + * data_server.hpp + * + * Transmit DMA data via TCP server socket. + * + * Joris van Rantwijk 2024 + */ + +#ifndef PUZZLEFW_DATA_SERVER_H_ +#define PUZZLEFW_DATA_SERVER_H_ + +#include +#include +#include +#include +#include +#include + +#include +#include + +#include "logging.hpp" +#include "puzzlefw.hpp" + + +namespace puzzlefw { + + +namespace asio = boost::asio; + + +/** + * Send DMA data via a TCP server socket. + */ +class DataServer +{ +public: + /** Maximum block size per TCP send() call. */ + static constexpr size_t SEND_MAX_BLOCK = 65536; + + /** Sleep until this number of bytes is available in the buffer. */ + static constexpr size_t WAIT_BLOCK_SIZE = 4096; + + /** Sleep at most this duration if there is insufficient data. */ + static constexpr std::chrono::duration WAIT_TIMEOUT = + std::chrono::milliseconds(10); + + /** Constructor. */ + DataServer( + asio::io_context& io, + DmaWriteStream& dma_stream, + uint16_t tcp_port) + : m_strand(asio::make_strand(io)) + , m_acceptor(m_strand) + , m_connection(m_strand) + , m_timer(m_strand) + , m_dma_stream(dma_stream) + , m_tcp_port(tcp_port) + , m_stale_receive(false) + , m_stale_send(false) + , m_send_in_progress(false) + { + if (dma_stream.buffer_segment_size() < 2 * WAIT_BLOCK_SIZE + || SEND_MAX_BLOCK % dma_stream.dma_alignment() != 0 + || WAIT_BLOCK_SIZE % dma_stream.dma_alignment() != 0) { + throw std::invalid_argument("Invalid buffer segment size"); + } + } + + // Delete copy constructor and assignment operator. + DataServer(const DataServer&) = delete; + DataServer& operator=(const DataServer&) = delete; + + /** Destructor. */ + ~DataServer() + { + // Disable interrupts. + m_dma_stream.disable_interrupt(); + } + + /** Start the server. */ + void start_server() + { + // If the server is already open, close and re-open it. + if (m_acceptor.is_open()) { + m_acceptor.close(); + } + + // Open IPv6 TCP socket. + m_acceptor.open(asio::ip::tcp::v6()); + + // Disable IPV6_V6ONLY to allow IPv4 connections. + m_acceptor.set_option(asio::ip::v6_only(false)); + + // Enable SO_REUSEADDR. + m_acceptor.set_option(asio::socket_base::reuse_address(true)); + + // Bind to the TCP port on all interfaces. + asio::ip::tcp::endpoint addr(asio::ip::address_v6::any(), m_tcp_port); + m_acceptor.bind(addr); + + // Start listening for connections. + m_acceptor.listen(); + + // Asynchronously accept connections. + m_acceptor.async_accept( + [this](auto& ec, auto s){ handle_accept(ec, std::move(s)); }); + + log(LOG_INFO, "Ready for TCP connections to port %d", m_tcp_port); + } + + /** Stop the server and close current connections. */ + void stop_server() + { + // Stop accepting connections. + if (m_acceptor.is_open()) { + log(LOG_INFO, "Closing TCP server on port %d", m_tcp_port); + m_acceptor.close(); + } + + // Close the current connection. + if (m_connection.is_open()) { + log(LOG_INFO, "Closing connection to port %d", m_tcp_port); + m_connection.close(); + m_stale_receive = true; + m_stale_send = m_send_in_progress; + } + } + + /** + * Called when an FPGA interrupt occurs. + * + * The interrupt may or may not be related to our DMA stream. + * Before returning, this function must disable and clear any pending + * interrupt for the DMA stream. + * + * This function may be called outside the strand of this instance. + */ + void handle_interrupt() + { + if (m_dma_stream.is_interrupt_pending()) { + m_dma_stream.disable_interrupt(); + asio::post(m_strand, + [this](){ handle_interrupt_in_strand(); }); + } + } + +private: + /** Accept completion handler. */ + void handle_accept(const boost::system::error_code& error, + asio::ip::tcp::socket conn) + { + if (error) { + // Ignore error due to cancellation. + if (error == asio::error::operation_aborted) { + return; + } + + // Certain errors can be triggered by network conditions. + // In these cases, we should retry the accept call. + if (error == asio::error::broken_pipe + || error == asio::error::connection_aborted + || error == asio::error::connection_reset + || error == asio::error::host_unreachable + || error == asio::error::network_down + || error == asio::error::network_reset + || error == asio::error::network_unreachable + || error == asio::error::timed_out) { + + log(LOG_ERROR, + "Accept failed for port %d (%s), retrying", + m_tcp_port, + error.message().c_str()); + + // Retry accept call. + if (m_acceptor.is_open()) { + m_acceptor.async_accept( + [this](auto& ec, auto s) { + handle_accept(ec, std::move(s)); + }); + } + + return; + } + + // Raise exception on unexpected error. + throw std::system_error(error); + } + + if (! m_acceptor.is_open()) { + // Oops we were not supposed to accept new connections. + // Apparently this connection sneaked right through before + // closing the acceptor socket. + // Drop the new connection. + log(LOG_INFO, "Dropping new connection to port %d", m_tcp_port); + conn.close(); + return; + } + + if (m_acceptor.is_open()) { + // Continue accepting connections. + m_acceptor.async_accept( + [this](auto& ec, auto s){ handle_accept(ec, std::move(s)); }); + } + + if (m_connection.is_open()) { + // We already had an active client connection. + // Drop the old connection and switch to the new client. + log(LOG_INFO, + "Closing current connection to port %d", m_tcp_port); + m_connection.close(); + m_stale_receive = true; + m_stale_send = m_send_in_progress; + } + + log(LOG_INFO, "New connection to port %d", m_tcp_port); + m_connection = std::move(conn); + + if (! m_stale_receive) { + // Setup async receive to detect when the connection is + // closed remotely (or client writes unexpected data). + m_connection.async_receive( + asio::buffer(m_receive_buf, sizeof(m_receive_buf)), + [this](auto& ec, size_t n){ handle_receive(ec, n); }); + } + + // Try to send some data. + transmit_data(false); + } + + /** Receive completion handler. */ + void handle_receive(const boost::system::error_code& error, size_t len) + { + if (m_stale_receive) { + // This completion refers to an old, already closed connection. + m_stale_receive = false; + + if (m_connection.is_open()) { + // Initiate async receive for the new connection. + m_connection.async_receive( + asio::buffer(m_receive_buf, sizeof(m_receive_buf)), + [this](auto& ec, size_t n){ handle_receive(ec, n); }); + } + + return; + } + + // Either the connection was closed remotely, or a network error + // occurred, or the remote side sent us unexpected data. + // In all of these cases, this connection must be closed. + + // Report what happened. + if (error) { + if (error == asio::error::eof) { + log(LOG_INFO, + "Connection to port %d closed by remote", m_tcp_port); + } else { + log(LOG_ERROR, + "Receive failed on port %d (%s), closing connection", + m_tcp_port, + error.message().c_str()); + } + } else { + log(LOG_ERROR, + "Received unexpected data on port %d, closing connection", + m_tcp_port); + } + + // Close connection. + if (m_connection.is_open()) { + m_connection.close(); + m_stale_send = m_send_in_progress; + } + } + + /** Send completion handler. */ + void handle_send(const boost::system::error_code& error, size_t len) + { + m_send_in_progress = false; + + if (m_stale_send) { + // This completion refers to an old, already closed connection. + m_stale_send = false; + + // Discard the remaining data that was part of this send. + send_completed(); + return; + } + + if (error) { + // Report error. + if (error == asio::error::broken_pipe + || error == asio::error::connection_reset) { + log(LOG_INFO, + "Connection to port %d closed by remote", m_tcp_port); + } else { + log(LOG_ERROR, + "Send failed on port %d (%s), closing connection", + m_tcp_port, + error.message().c_str()); + } + + // Close the connection. + if (m_connection.is_open()) { + m_connection.close(); + m_stale_receive = true; + } + + // Discard the remaining data that was part of this send. + send_completed(); + return; + } + + if (len < m_send_buffer.size()) { + // Partially completed. Send the rest. + m_send_buffer += len; + m_send_in_progress = true; + m_connection.async_send( + m_send_buffer, + [this](auto& ec, size_t n){ handle_send(ec, n); }); + } else { + // Fully completed. + send_completed(); + } + } + + /** Called when a send operation is fully completed. */ + void send_completed() + { + assert(! m_send_in_progress); + assert(m_send_size > 0); + + // Release the completed part of the DMA buffer. + m_dma_stream.consume_data(m_send_size); + + // Try to send more data. + transmit_data(false); + } + + /** + * Try to send data from the DMA buffer to the TCP connection. + * + * If insufficient data is available in the buffer, setup + * to be notified when there is sufficient data. + */ + void transmit_data(bool skip_waiting) + { + if (m_send_in_progress) { + // Send already in progress. Do nothing until it completes. + return; + } + + if (! m_connection.is_open()) { + // No connection. Do nothing until we get a new connection. + return; + } + + // Check amount of data available. + size_t data_available = m_dma_stream.get_data_available(); + if (data_available == 0 || + (data_available < WAIT_BLOCK_SIZE && (! skip_waiting))) { + + // Insufficient data available. Setup interrupt. + m_dma_stream.enable_interrupt_on_data_available(WAIT_BLOCK_SIZE); + + // Double-check if data are already available. + // This is necessary to prevent a race condition where the data + // becomes available just before the interrupt is enabled. + data_available = m_dma_stream.get_data_available(); + if (data_available < WAIT_BLOCK_SIZE) { + + // Setup timeout in case interrupt takes too long. + // If timeout occurs, we will send whatever data is available. + m_timer.expires_after(WAIT_TIMEOUT); + m_timer.async_wait( + [this](auto& ec){ handle_timer(ec); }); + + // Done. We will be notified for interrupt or timeout. + return; + + } else { + + // We have enough data after all. Cancel the interrupt. + m_dma_stream.disable_interrupt(); + + } + } + + // Get a continuous data block. + void *data; + m_dma_stream.get_data_block(data, data_available); + + // Initiate async send. + // Limit the block size so we can release that part of the buffer + // as soon as it completes. + m_send_size = std::min(data_available, SEND_MAX_BLOCK); + m_send_buffer = asio::buffer(data, m_send_size); + m_send_in_progress = true; + m_connection.async_send( + m_send_buffer, + [this](auto& ec, size_t n){ handle_send(ec, n); }); + } + + /** Timeout handler. */ + void handle_timer(const boost::system::error_code& error) + { + if (error) { + // Ignore error due to cancellation. + if (error == asio::error::operation_aborted) { + return; + } + + // Raise exception on unexpected error. + throw std::system_error(error); + } + + // We get here if a timeout occurs while waiting for an interrupt. + // Disable the interrupt because we no longer care about it. + m_dma_stream.disable_interrupt(); + + // Try to send some data. + transmit_data(true); + } + + /** + * Called when an FPGA interrupt occurs for our DMA stream. + * + * This function runs in the strand of this instance. + */ + void handle_interrupt_in_strand() + { + // Cancel the interrupt timeout. + m_timer.cancel(); + + // Try to send some data. + transmit_data(true); + } + + asio::strand m_strand; + asio::ip::tcp::acceptor m_acceptor; + asio::ip::tcp::socket m_connection; + asio::steady_timer m_timer; + DmaWriteStream& m_dma_stream; + const uint16_t m_tcp_port; + char m_receive_buf[1]; + bool m_stale_receive; + bool m_stale_send; + bool m_send_in_progress; + size_t m_send_size; + asio::const_buffer m_send_buffer; +}; + + +/** + * Monitor DMA status and throw exception if an error occurs. + */ +class DmaErrorMonitor +{ +public: + /** Constructor. */ + DmaErrorMonitor( + asio::io_context& io, + PuzzleFwDevice& device, + const std::chrono::milliseconds interval) + : m_timer(io) + , m_device(device) + , m_interval(interval) + { + m_timer.expires_after(m_interval); + m_timer.async_wait([this](auto& ec){ handle_timer(ec); }); + } + + // Delete copy constructor and assignment operator. + DmaErrorMonitor(const DmaErrorMonitor&) = delete; + DmaErrorMonitor& operator=(const DmaErrorMonitor&) = delete; + + /** + * Check whether DMA errors occurred. + * + * Throws std::runtime_error() if a DMA error is pending. + */ + void check_dma_error() + { + uint32_t dma_status = m_device.get_dma_status(); + if ((dma_status & 0x1e) != 0) { + std::stringstream msg; + msg << "DMA error: status=0x" + << std::setfill('0') << std::setw(2) << std::hex + << dma_status; + throw std::runtime_error(msg.str()); + } + } + +private: + /** Timeout handler. */ + void handle_timer(const boost::system::error_code& error) + { + if (error) { + // Ignore error due to cancellation. + if (error == asio::error::operation_aborted) { + return; + } + + // Raise exception on unexpected error. + throw std::system_error(error); + } + + m_timer.expires_after(m_interval); + m_timer.async_wait([this](auto& ec){ handle_timer(ec); }); + + check_dma_error(); + } + + asio::steady_timer m_timer; + PuzzleFwDevice& m_device; + std::chrono::milliseconds m_interval; +}; + + +} // namespace puzzlefw + +#endif // PUZZLEFW_DATA_SERVER_H_ diff --git a/os/src/userspace/interrupt_manager.hpp b/os/src/userspace/interrupt_manager.hpp new file mode 100644 index 0000000..49f9449 --- /dev/null +++ b/os/src/userspace/interrupt_manager.hpp @@ -0,0 +1,114 @@ +/* + * interrupt_manager.hpp + * + * Interrupt handling for PuzzleFW firmware. + * + * Joris van Rantwijk 2024 + */ + +#ifndef PUZZLEFW_INTERRUPT_MANAGER_H_ +#define PUZZLEFW_INTERRUPT_MANAGER_H_ + +#include +#include +#include + +#include +#include + +#include "puzzlefw.hpp" + + +namespace puzzlefw { + + +namespace asio = boost::asio; + + +/** + * Handle FPGA interrupts via the UIO file descriptor. + * + * When an interrupt occurs, a list of callback functions are invoked. + */ +class InterruptManager +{ +public: + /** Start handling interrupts. */ + InterruptManager(asio::io_context& io, PuzzleFwDevice& device) + : m_device(device) + , m_uio_fd(io, device.uio_fd()) + { + // Enable FPGA interrupts. + m_device.enable_irq(); + + // Start asynchronous wait on the UIO file descriptor. + m_uio_fd.async_wait(asio::posix::descriptor_base::wait_read, + [this](auto& ec){ handle_wait(ec); }); + } + + // Delete copy constructor and assignment operator. + InterruptManager(const InterruptManager&) = delete; + InterruptManager& operator=(const InterruptManager&) = delete; + + /** Destructor. */ + ~InterruptManager() + { + // Cancel asynchronous wait. + m_uio_fd.cancel(); + + // Release UIO file descriptor (without closing it). + m_uio_fd.release(); + } + + /** + * Add callback function to invoke when an interrupt occurs. + * + * The callback functions are (together) responsible for disabling + * and clearing all pending interrupts. + */ + template + void add_callback(Func&& func) + { + m_callbacks.emplace_back(func); + } + +private: + /** Called when an FPGA interrupt occurs. */ + void handle_wait(const boost::system::error_code& error) + { + if (error) { + // Ignore error due to cancellation. + if (error == asio::error::operation_aborted) { + return; + } + + // Raise exception on unexpected error. + throw std::system_error(error); + } + + // Read from file descriptor to clear interrupt. + char buf[4]; + ::read(m_uio_fd.native_handle(), buf, 4); + + // Invoke callbacks. + for (const auto& callback : m_callbacks) { + callback(); + } + + // Re-enable FPGA interrupts. + m_device.enable_irq(); + + // Wait for next interrupt. + m_uio_fd.async_wait(asio::posix::descriptor_base::wait_read, + [this](auto& ec){ handle_wait(ec); }); + } + + PuzzleFwDevice& m_device; + asio::posix::stream_descriptor m_uio_fd; + std::vector> m_callbacks; +}; + + +} // namespace puzzlefw + +#endif // PUZZLEFW_INTERRUPT_MANAGER_H_ diff --git a/os/src/userspace/logging.hpp b/os/src/userspace/logging.hpp new file mode 100644 index 0000000..fd3fa69 --- /dev/null +++ b/os/src/userspace/logging.hpp @@ -0,0 +1,59 @@ +/* + * logging.hpp + * + * Log messages to stdout. + * + * Joris van Rantwijk 2024 + */ + +#ifndef PUZZLEFW_LOGGING_H_ +#define PUZZLEFW_LOGGING_H_ + +#include +#include +#include + + +namespace puzzlefw { + + +enum log_severity { LOG_INFO = 1, LOG_ERROR }; + + +void log(log_severity severity, const char *format, ...) + __attribute__ ((format (printf, 2, 3))); + + +void log(log_severity severity, const char *format, ...) +{ + va_list ap; + va_start(ap, format); + + struct timespec tp; + clock_gettime(CLOCK_REALTIME, &tp); + + char datestr[80]; + struct tm tlocal; + strftime(datestr, sizeof(datestr), + "%Y-%m-%d %H:%M:%S", + localtime_r(&tp.tv_sec, &tlocal)); + + const char *severity_str = (severity == LOG_INFO) ? "INFO " : "ERROR"; + + char buf[800]; + vsnprintf(buf, sizeof(buf), format, ap); + + printf("%s.%03d (%s) %s\n", + datestr, + (int)(tp.tv_nsec / 1000000), + severity_str, + buf); + fflush(stdout); + + va_end(ap); +} + + +} // namespace puzzlefw + +#endif // PUZZLEFW_LOGGING_H_ diff --git a/os/src/userspace/puzzlecmd.cpp b/os/src/userspace/puzzlecmd.cpp new file mode 100644 index 0000000..7aec530 --- /dev/null +++ b/os/src/userspace/puzzlecmd.cpp @@ -0,0 +1,599 @@ +/* + * puzzlecmd.cpp + * + * Command-line program to test PuzzleFW firmware. + */ + +#include +#include +#include +#include +#include + +#include +#include + +#include "puzzlefw.hpp" +#include "interrupt_manager.hpp" +#include "data_server.hpp" + + +/** Convert TriggerMode to string description. */ +std::string trigger_mode_to_string(puzzlefw::TriggerMode mode) +{ + using puzzlefw::TriggerMode; + switch (mode) { + case TriggerMode::TRIG_AUTO: return "auto"; + case TriggerMode::TRIG_EXTERNAL: return "external"; + default: return "none"; + } +} + + +/** Show firmware status. */ +void show_status(puzzlefw::PuzzleFwDevice& device) +{ + printf("Status:\n"); + + printf(" timestamp = %llu\n", + (unsigned long long)device.get_timestamp()); + + for (unsigned int i = 0; i < device.get_analog_channel_count(); i++) { + unsigned int sample, min_sample, max_sample; + sample = device.get_adc_sample(i); + device.get_adc_range(i, min_sample, max_sample); + printf(" channel %u = %5u (min = %u, max = %u)\n", + i, sample, min_sample, max_sample); + } + + uint32_t digital_state = device.get_digital_input_state(); + printf(" digital input = %u %u %u %u\n", + digital_state & 1, + (digital_state >> 1) & 1, + (digital_state >> 2) & 1, + (digital_state >> 3) & 1); + + printf(" acquisition = %s\n", + device.is_acquisition_enabled() ? "on" : "off"); + + printf(" channel mode = %d channels", + device.is_4channel_mode() ? 4 : 2); + + printf(" trigger mode = %s, channel=%u, edge=%s\n", + trigger_mode_to_string(device.get_trigger_mode()).c_str(), + device.get_trigger_ext_channel(), + device.get_trigger_ext_falling() ? "falling" : "rising"); + + printf(" trigger delay = %u * 8 ns\n", + device.get_trigger_delay()); + + printf(" record length = %u samples\n", + device.get_record_length()); + + unsigned int divisor = device.get_decimation_factor(); + printf(" rate divisor = %u, sample rate = %u Sa/s\n", + divisor, 125000000 / divisor); + + printf(" averaging = %s\n", + device.is_averaging_enabled() ? "on" : "off"); + + printf(" shift steps = %u\n", + device.get_shift_steps()); + + printf(" timetagger mask = 0x%02x\n", + device.get_timetagger_event_mask()); + + printf(" ADC simulation = %s\n", + device.is_adc_simulation_enabled() ? "on" : "off"); + + if (device.is_digital_simulation_enabled()) { + uint32_t simulation_state = device.get_digital_simulation_state(); + printf(" digital simulation = %u %u %u %u\n", + simulation_state & 1, + (simulation_state >> 1) & 1, + (simulation_state >> 2) & 1, + (simulation_state >> 3) & 1); + } else { + printf(" digital simulation = off\n"); + } + + uint32_t dma_status = device.get_dma_status(); + printf(" DMA status = %s, %s%s%s%s\n", + device.is_dma_enabled() ? "enabled" : "disabled", + (dma_status & 1) ? "busy" : "idle", + (dma_status & 2) ? ", READ ERROR" : "", + (dma_status & 4) ? ", WRITE ERROR" : "", + (dma_status & 8) ? ", ADDRESS ERROR" : ""); +} + + +/** Run TCP data server. */ +void run_data_server(puzzlefw::PuzzleFwDevice& device) +{ + namespace asio = boost::asio; + using namespace puzzlefw; + + asio::io_context io; + + // Reserve 3/4 of the DMA buffer for analog acquisition data. + // Reserve 1/4 of the DMA buffer for timetagger data. + size_t acq_buf_size = 3 * 4096 * (device.dma_buffer_size() / 4096 / 4); + size_t timetagger_buf_size = device.dma_buffer_size() - acq_buf_size; + + DmaWriteStream acq_stream( + device, + DmaWriteStream::DMA_ACQ, + 0, + acq_buf_size); + DmaWriteStream timetagger_stream( + device, + DmaWriteStream::DMA_TT, + acq_buf_size, + acq_buf_size + timetagger_buf_size); + + DataServer acq_server(io, acq_stream, 5001); + DataServer timetagger_server(io, timetagger_stream, 5002); + + InterruptManager interrupt_manager(io, device); + interrupt_manager.add_callback( + [&acq_server](){ acq_server.handle_interrupt(); }); + interrupt_manager.add_callback( + [&timetagger_server](){ timetagger_server.handle_interrupt(); }); + + DmaErrorMonitor dma_error_monitor( + io, + device, + std::chrono::milliseconds(100)); + + // Clear DMA errors, then enable DMA engine. + device.clear_dma_errors(); + device.set_dma_enabled(true); + + acq_server.start_server(); + acq_stream.set_enabled(true); +// TODO -- timetagger + + io.run(); +} + + +/** Parse integer. */ +int parse_int(const char *arg, bool& ok) +{ + if (*arg == 0) { + ok = false; + return 0; + } + char *end; + long v = strtol(arg, &end, 0); + if (*end != 0) { + ok = false; + return 0; + } + if (v < INT_MIN || v > INT_MAX) { + ok = false; + return 0; + } + ok = true; + return v; +} + + +int main(int argc, char **argv) +{ + using puzzlefw::PuzzleFwDevice; + using puzzlefw::VersionInfo; + + enum Option { + OPT_SHOW = 1, + OPT_CLEAR_DMA, OPT_CLEAR_TIMESTAMP, OPT_CLEAR_RANGE, + OPT_ACQUISITION_ON, OPT_ACQUISITION_OFF, OPT_CHANNELS, + OPT_TRIGGER_NONE, OPT_TRIGGER_AUTO, OPT_TRIGGER_EXT, + OPT_TRIGGER_CHANNEL, OPT_TRIGGER_RISING, OPT_TRIGGER_FALLING, + OPT_TRIGGER_DELAY, OPT_TRIGGER, + OPT_RECORD_LEN, OPT_DIVISOR, OPT_AVERAGE, OPT_DECIMATE, OPT_SHIFT, + OPT_TIMETAGGER_CHANNELS, OPT_MARKER, + OPT_ADC_SIM, OPT_DIG_SIM, + OPT_SERVER + }; + + static const struct option options[] = { + {"help", 0, 0, 'h'}, + {"show", 0, 0, OPT_SHOW}, + {"clear-dma", 0, 0, OPT_CLEAR_DMA}, + {"clear-timestamp", 0, 0, OPT_CLEAR_TIMESTAMP}, + {"clear-range", 0, 0, OPT_CLEAR_RANGE}, + {"acquisition-on", 0, 0, OPT_ACQUISITION_ON}, + {"acquisition-off", 0, 0, OPT_ACQUISITION_OFF}, + {"channels", 1, 0, OPT_CHANNELS}, + {"trigger-none", 0, 0, OPT_TRIGGER_NONE}, + {"trigger-auto", 0, 0, OPT_TRIGGER_AUTO}, + {"trigger-ext", 0, 0, OPT_TRIGGER_EXT}, + {"trigger-channel", 1, 0, OPT_TRIGGER_CHANNEL}, + {"trigger-rising", 0, 0, OPT_TRIGGER_RISING}, + {"trigger-falling", 0, 0, OPT_TRIGGER_FALLING}, + {"trigger-delay", 1, 0, OPT_TRIGGER_DELAY}, + {"trigger", 0, 0, OPT_TRIGGER}, + {"record-len", 1, 0, OPT_RECORD_LEN}, + {"divisor", 1, 0, OPT_DIVISOR}, + {"average", 0, 0, OPT_AVERAGE}, + {"decimate", 0, 0, OPT_DECIMATE}, + {"shift", 1, 0, OPT_SHIFT}, + {"timetagger", 1, 0, OPT_TIMETAGGER_CHANNELS}, + {"marker", 0, 0, OPT_MARKER}, + {"adc-sim", 1, 0, OPT_ADC_SIM}, + {"dig-sim", 1, 0, OPT_DIG_SIM}, + {"server", 0, 0, OPT_SERVER}, + {nullptr, 0, 0, 0} + }; + + const char *usage_text = + "Usage: %s {options...}\n" + "Try '--help' for more information.\n"; + + const char *help_text = + "Usage: %s {options...}\n" + "Command-line program to test PuzzleFW firmware.\n" + "\n" + "Options:\n" + " --help, -h Show this help message.\n" + " --show Show current firmware state.\n" + " --clear-dma Stop DMA and clear DMA errors.\n" + " --clear-timestamp Reset timestamp counter to zero.\n" + " --clear-range Clear ADC min/max sample monitor.\n" + " --acquisition-on Enable analog acquisition.\n" + " --acquisition-off Disable analog acquisition.\n" + " --channels 2|4 Select 2-channel or 4-channel mode.\n" + " --trigger-none Disable triggering.\n" + " --trigger-auto Enable continuous triggering.\n" + " --trigger-ext Enable external trigger.\n" + " --trigger-channel N Select trigger input channel (range 0 to 3)." + "\n" + " --trigger-rising Trigger on rising edge.\n" + " --trigger-falling Trigger on falling edge.\n" + " --trigger-delay N Set trigger delay to N * 8 ns.\n" + " --trigger Send a manual trigger event.\n" + " --record-len N Set record length to N samples per trigger." + "\n" + " --divisor N Set sample rate to 125 MSa/s divided by N." + "\n" + " --average Reduce sample rate by summing samples.\n" + " --decimate Reduce sample rate by decimating samples.\n" + " --shift N Shift sample values right by N positions.\n" + " --timetagger MASK Set bit mask of enabled timetagger events.\n" + " 0 = all events disabled\n" + " 1 = enable rising edge on channel 0\n" + " 2 = enable falling edge on channel 0\n" + " 4 = enable rising edge on channel 1\n" + " ... 255 = all event types enabled\n" + " --marker Emit a marker in the timetagger stream.\n" + " --adc-sim 0|1 Enable (1) or disable (0) ADC simulation.\n" + " --dig-sim MASK|off Set simulated digital input state as bitmask" + "\n" + " of channels, or disable simulation.\n" + " --server Run TCP server to send data.\n" + "\n"; + + struct { + bool show; + bool clear_dma; + bool clear_timestamp; + bool clear_range; + std::optional acquisition_en; + std::optional channels; + std::optional trigger_mode; + std::optional trigger_channel; + std::optional trigger_falling; + std::optional trigger_delay; + bool trigger_force; + std::optional record_len; + std::optional divisor; + std::optional averaging_en; + std::optional shift_steps; + std::optional timetagger_channels; + bool marker; + std::optional adc_sim; + std::optional dig_sim; + bool server; + } args = {}; + + while (1) { + int c = getopt_long(argc, argv, "h", options, nullptr); + if (c == -1) { + break; + } + + bool ok; + switch (c) { + case 'h': + printf(help_text, argv[0]); + return 0; + case OPT_SHOW: + args.show = true; + break; + case OPT_CLEAR_DMA: + args.clear_dma = true; + break; + case OPT_CLEAR_TIMESTAMP: + args.clear_timestamp = true; + break; + case OPT_CLEAR_RANGE: + args.clear_range = true; + break; + case OPT_ACQUISITION_ON: + args.acquisition_en = true; + break; + case OPT_ACQUISITION_OFF: + args.acquisition_en = true; + break; + case OPT_CHANNELS: + args.channels = parse_int(optarg, ok); + if (!ok || (args.channels != 2 && args.channels != 4)) { + fprintf(stderr, "ERROR: Invalid value for --channels\n"); + return 1; + } + break; + case OPT_TRIGGER_NONE: + args.trigger_mode = puzzlefw::TRIG_NONE; + break; + case OPT_TRIGGER_AUTO: + args.trigger_mode = puzzlefw::TRIG_AUTO; + break; + case OPT_TRIGGER_EXT: + args.trigger_mode = puzzlefw::TRIG_EXTERNAL; + break; + case OPT_TRIGGER_CHANNEL: + args.trigger_channel = parse_int(optarg, ok); + if (!ok + || args.trigger_channel < 0 + || args.trigger_channel > 3) { + fprintf(stderr, + "ERROR: Invalid value for --trigger-channel\n"); + return 1; + } + break; + case OPT_TRIGGER_RISING: + args.trigger_falling = false; + break; + case OPT_TRIGGER_FALLING: + args.trigger_falling = true; + break; + case OPT_TRIGGER_DELAY: + args.trigger_delay = parse_int(optarg, ok); + if (!ok + || args.trigger_delay < 0 + || args.trigger_delay > PuzzleFwDevice::MAX_TRIGGER_DELAY) + { + fprintf(stderr, + "ERROR: Invalid value for --trigger-delay\n"); + return 1; + } + break; + case OPT_TRIGGER: + args.trigger_force = true; + break; + case OPT_RECORD_LEN: + args.record_len = parse_int(optarg, ok); + if (!ok + || args.record_len < 1 + || args.record_len > PuzzleFwDevice::MAX_RECORD_LENGTH) { + fprintf(stderr, + "ERROR: Invalid value for --record-len\n"); + return 1; + } + break; + case OPT_DIVISOR: + args.divisor = parse_int(optarg, ok); + if (!ok + || args.divisor < 1 + || args.divisor > PuzzleFwDevice::MAX_DECIMATION_FACTOR) { + fprintf(stderr, + "ERROR: Invalid value for --divisor\n"); + return 1; + } + break; + case OPT_AVERAGE: + args.averaging_en = true; + break; + case OPT_DECIMATE: + args.averaging_en = false; + break; + case OPT_SHIFT: + args.shift_steps = parse_int(optarg, ok); + if (!ok || args.shift_steps < 0 || args.shift_steps > 8) { + fprintf(stderr, + "ERROR: Invalid value for --shift-steps\n"); + return 1; + } + break; + case OPT_TIMETAGGER_CHANNELS: + args.timetagger_channels = parse_int(optarg, ok); + if (!ok + || args.timetagger_channels < 0 + || args.timetagger_channels > 255) { + fprintf(stderr, + "ERROR: Invalid value for --timetagger\n"); + return 1; + } + break; + case OPT_MARKER: + args.marker = true; + break; + case OPT_ADC_SIM: + if (std::string("0") == optarg + || std::string("off") == optarg) { + args.adc_sim = false; + } else if (std::string("1") == optarg + || std::string("on") == optarg) { + args.adc_sim = true; + } else { + fprintf(stderr, + "ERROR: Invalid value for --adc-sim\n"); + return 1; + } + break; + case OPT_DIG_SIM: + if (std::string("off") == optarg) { + args.dig_sim = -1; + } else { + args.dig_sim = parse_int(optarg, ok); + if (!ok || args.dig_sim < 0 || args.dig_sim > 15) { + fprintf(stderr, + "ERROR: Invalid value for --dig-sim\n"); + return 1; + } + } + break; + case OPT_SERVER: + args.server = true; + break; + default: + fprintf(stderr, usage_text, argv[0]); + return 1; + } + } + + if (optind < argc) { + fprintf(stderr, "ERROR: Unexpected positional argument\n"); + fprintf(stderr, usage_text, argv[0]); + return 1; + } + + try { + PuzzleFwDevice device; + + VersionInfo version = device.get_version_info(); + printf("Detected PuzzleFW firmware version %d.%d\n", + version.major_version, version.minor_version); + printf(" %u analog input channels\n", + device.get_analog_channel_count()); + printf(" DMA buffer size: %zu bytes\n", + device.dma_buffer_size()); + + if (args.clear_dma) { + printf("Disabling and clearing DMA engine ...\n"); + device.set_dma_enabled(false); + device.clear_dma_errors(); + } + + if (args.clear_timestamp) { + printf("Resetting timestamp counter ...\n"); + device.clear_timestamp(); + } + + if (args.clear_range) { + printf("Resetting ADC min/max sample monitor ...\n"); + device.clear_adc_range(); + } + + if (args.acquisition_en.has_value()) { + printf("%s analog acquisition ...\n", + args.acquisition_en.value() ? "Enabling" : "Disabling"); + device.set_acquisition_enabled(args.acquisition_en.value()); + } + + if (args.record_len.has_value()) { + printf("Setting record length to %d samples per trigger ...\n", + args.record_len.value()); + device.set_record_length(args.record_len.value()); + } + + if (args.divisor.has_value()) { + printf("Selecting sample rate divisor %d ...\n", + args.divisor.value()); + device.set_decimation_factor(args.divisor.value()); + } + + if (args.averaging_en.has_value()) { + printf("Selecting downsampling via %s ...\n", + args.averaging_en.value() ? "averaging" : "decimation"); + device.set_averaging_enabled(args.averaging_en.value()); + } + + if (args.channels.has_value()) { + printf("Selecting %d-channel mode ...\n", args.channels.value()); + device.set_4channel_mode(args.channels.value() == 4); + } + + if (args.shift_steps.has_value()) { + printf("Selecting %d right-shift steps ...\n", + args.shift_steps.value()); + device.set_shift_steps(args.shift_steps.value()); + } + + if (args.trigger_channel.has_value()) { + printf("Selecting trigger channel %d ...\n", + args.trigger_channel.value()); + device.set_trigger_ext_channel(args.trigger_channel.value()); + } + + if (args.trigger_falling.has_value()) { + printf("Selecting trigger on %s edge ...\n", + args.trigger_falling.value() ? "falling" : "rising"); + device.set_trigger_ext_falling(args.trigger_falling.value()); + } + + if (args.trigger_delay.has_value()) { + printf("Selecting trigger delay %d * 8 ns ...\n", + args.trigger_delay.value()); + device.set_trigger_delay(args.trigger_delay.value()); + } + + if (args.trigger_mode.has_value()) { + printf("Selecting trigger mode %s ...\n", + trigger_mode_to_string(args.trigger_mode.value()).c_str()); + device.set_trigger_mode(args.trigger_mode.value()); + } + + if (args.trigger_force) { + printf("Sending forced trigger ...\n"); + device.trigger_force(); + } + + if (args.timetagger_channels.has_value()) { + printf("Setting timetagger channel mask 0x%02x ...\n", + args.timetagger_channels.value()); + device.set_timetagger_event_mask( + args.timetagger_channels.value()); + } + + if (args.marker) { + printf("Emitting timetagger marker record ...\n"); + device.timetagger_mark(); + } + + if (args.adc_sim.has_value()) { + printf("%s ADC simulation ...\n", + args.adc_sim.value() ? "Enabling" : "Disabling"); + device.set_adc_simulation_enabled(args.adc_sim.value()); + } + + if (args.dig_sim.has_value()) { + if (args.dig_sim.value() < 0) { + printf("Disabling digital input simulation ...\n"); + device.set_digital_simulation_enabled(false); + } else { + printf("Setting digital input simulation state 0x%x ...\n", + (unsigned int)args.dig_sim.value()); + device.set_digital_simulation_state(args.dig_sim.value()); + device.set_digital_simulation_enabled(true); + } + } + + if (args.show) { + show_status(device); + } + + if (args.server) { + run_data_server(device); + } + + } catch (std::exception& e) { + fprintf(stderr, "ERROR: %s\n", e.what()); + return 1; + } + + return 0; +} + +/* end */ diff --git a/os/src/userspace/puzzlefw.cpp b/os/src/userspace/puzzlefw.cpp new file mode 100644 index 0000000..d77750d --- /dev/null +++ b/os/src/userspace/puzzlefw.cpp @@ -0,0 +1,171 @@ +/* + * puzzlefw.cpp + * + * C++ library for PuzzleFW firmware. + * + * Joris van Rantwijk 2024 + */ + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "puzzlefw.hpp" + + +namespace fs = std::filesystem; + + +/** Path to platform devices. */ +constexpr char SOC_DEVICES_DIR[] = "/sys/devices/soc0"; + + +/** + * Find PuzzleFW device in /sys filesystem. + * + * Return the device name. + * Throw std::runtime_error if an error occurs. + */ +static std::string find_puzzlefw_device_name() +{ + // Find entry matching "*.puzzlefw" in /sys filesystem. + for (const auto& entry : fs::directory_iterator(SOC_DEVICES_DIR)) { + std::string fname = entry.path().filename().string(); + if (fname.compare(fname.size() - 9, 9, ".puzzlefw") == 0) { + return fname; + } + } + + throw std::runtime_error( + std::string("Device *.puzzlefw not found in ") + SOC_DEVICES_DIR); +} + + +/** + * Find UIO device node for the PuzzleFW driver. + * + * Set "uio_path" to the full path to the UIO device node. + * Set "dma_buf_size" to the size of the global DMA buffer. + * + * Throw std::runtime_error if an error occurs. + */ +static void find_puzzlefw_uio_device(const std::string& device_name, + std::string& uio_path, + size_t& dma_buf_size) +{ + // Construct path to device "uio" directory. + fs::path uio_dir = fs::path(SOC_DEVICES_DIR) / device_name / "uio"; + + // Find entry matching "uio*". + fs::path uio_subdir; + for (const auto& entry : fs::directory_iterator(uio_dir)) { + fs::path p = entry.path(); + if (p.filename().string().compare(0, 3, "uio") == 0) { + uio_subdir = p; + } + } + + if (uio_subdir.empty()) { + throw std::runtime_error( + std::string("No UIO device node found in ") + uio_dir.string()); + } + + // Construct full path to "/dev/uioN" node. + fs::path uio_dev = fs::path("/dev") / uio_subdir.filename(); + uio_path = uio_dev.string(); + + // Read DMA buffer size. + std::ifstream map_size_if(uio_subdir / "maps" / "map1" / "size"); + unsigned long map_size; + map_size_if >> std::hex >> map_size; + dma_buf_size = map_size; +} + + +/* ******** class PuzzleFwDevice ******** */ + +/* Constructor. */ +puzzlefw::PuzzleFwDevice::PuzzleFwDevice() + : m_uio_fd(-1) + , m_regs(nullptr) + , m_dma_buf(nullptr) + , m_dma_buf_size(0) +{ + // Find platform device name of PuzzleFW firmware. + m_device_name = find_puzzlefw_device_name(); + + // Find UIO device node and DMA buffer size. + find_puzzlefw_uio_device(m_device_name, m_uio_path, m_dma_buf_size); + + if (m_dma_buf_size % dma_alignment() != 0 + || m_dma_buf_size < MIN_DMA_BUF_SIZE) { + throw std::runtime_error(std::string("Invalid DMA buffer size (") + + std::to_string(m_dma_buf_size) + ")"); + } + + // Open UIO device node. + // O_SYNC is necessary to map registers and DMA buffer as uncacheable. + m_uio_fd = open(m_uio_path.c_str(), O_RDWR | O_SYNC); + if (m_uio_fd < 0) { + throw std::system_error( + std::error_code(errno, std::system_category()), + std::string("Can not open UIO device") + m_uio_path); + } + + // Map FPGA user registers. + // Offset 0 tells the UIO to map the first address range, + // which corresponds to the FPGA registers. + void *regs = mmap(nullptr, + puzzlefw::REGS_SIZE, + PROT_READ | PROT_WRITE, + MAP_SHARED, + m_uio_fd, + 0); + if (regs == ((void*)-1)) { + close(m_uio_fd); + throw std::system_error( + std::error_code(errno, std::system_category()), + std::string("Can not open mmap registers from ") + m_uio_path); + } + + // Map DMA buffer. + // Offset PAGE_SIZE tells the UIO to map the second address range, + // which corresponds to the DMA buffer. + int page_size = getpagesize(); + void *dma_buf = mmap(nullptr, + m_dma_buf_size, + PROT_READ | PROT_WRITE, + MAP_SHARED, + m_uio_fd, + 1 * page_size); + if (dma_buf == ((void*)-1)) { + munmap(regs, puzzlefw::REGS_SIZE); + close(m_uio_fd); + throw std::system_error( + std::error_code(errno, std::system_category()), + std::string("Can not open mmap DMA buffer from ") + m_uio_path); + } + + m_regs = (uint32_t*)regs; + m_dma_buf = dma_buf; +} + + +/* Destructor. */ +puzzlefw::PuzzleFwDevice::~PuzzleFwDevice() +{ + // Disable DMA engine. + set_dma_enabled(false); + + // Unmap memory regions. + munmap((void*)m_regs, puzzlefw::REGS_SIZE); + munmap((void*)m_dma_buf, m_dma_buf_size); + + // Close UIO device. + close(m_uio_fd); +} diff --git a/os/src/userspace/puzzlefw.hpp b/os/src/userspace/puzzlefw.hpp new file mode 100644 index 0000000..98353d9 --- /dev/null +++ b/os/src/userspace/puzzlefw.hpp @@ -0,0 +1,899 @@ +/* + * puzzlefw.hpp + * + * C++ library for PuzzleFW firmware. + * + * Joris van Rantwijk 2024 + */ + +#ifndef PUZZLEFW_H_ +#define PUZZLEFW_H_ + +#include +#include + + +namespace puzzlefw { + +/* Size of memory mapped register area. */ +constexpr uint32_t REGS_SIZE = 0x1000; + +/* Register addresses relative to register area base address. */ +constexpr uint32_t REG_INFO = 0x000; +constexpr uint32_t REG_IRQ_ENABLE = 0x010; +constexpr uint32_t REG_IRQ_PENDING = 0x014; +constexpr uint32_t REG_DMA_EN = 0x100; +constexpr uint32_t REG_DMA_STATUS = 0x104; +constexpr uint32_t REG_DMA_CLEAR = 0x108; +constexpr uint32_t REG_TIMESTAMP_LO = 0x180; +constexpr uint32_t REG_TIMESTAMP_HI = 0x184; +constexpr uint32_t REG_TIMESTAMP_CLEAR = 0x188; +constexpr uint32_t REG_ACQ_ADDR_START = 0x200; +constexpr uint32_t REG_ACQ_ADDR_END = 0x204; +constexpr uint32_t REG_ACQ_ADDR_LIMIT = 0x208; +constexpr uint32_t REG_ACQ_ADDR_INTR = 0x20c; +constexpr uint32_t REG_ACQ_ADDR_PTR = 0x210; +constexpr uint32_t REG_ACQ_DMA_CTRL = 0x214; +constexpr uint32_t REG_ACQ_INTR_CTRL = 0x218; +constexpr uint32_t REG_ACQ_DMA_STATUS = 0x21c; +constexpr uint32_t REG_ACQUISITION_EN = 0x220; +constexpr uint32_t REG_RECORD_LENGTH = 0x224; +constexpr uint32_t REG_DECIMATION_FACTOR = 0x228; +constexpr uint32_t REG_SHIFT_STEPS = 0x22c; +constexpr uint32_t REG_AVERAGING_EN = 0x230; +constexpr uint32_t REG_CH4_MODE = 0x234; +constexpr uint32_t REG_SIMULATE_ADC = 0x238; +constexpr uint32_t REG_TRIGGER_MODE = 0x240; +constexpr uint32_t REG_TRIGGER_DELAY = 0x244; +constexpr uint32_t REG_TRIGGER_STATUS = 0x248; +constexpr uint32_t REG_ADC_SAMPLE = 0x280; +constexpr uint32_t REG_ADC23_SAMPLE = 0x284; +constexpr uint32_t REG_ADC_RANGE_CLEAR = 0x28c; +constexpr uint32_t REG_ADC0_MINMAX = 0x290; +constexpr uint32_t REG_ADC1_MINMAX = 0x294; +constexpr uint32_t REG_ADC2_MINMAX = 0x298; +constexpr uint32_t REG_ADC3_MINMAX = 0x29c; +constexpr uint32_t REG_TT_ADDR_START = 0x300; +constexpr uint32_t REG_TT_ADDR_END = 0x304; +constexpr uint32_t REG_TT_ADDR_LIMIT = 0x308; +constexpr uint32_t REG_TT_ADDR_INTR = 0x30c; +constexpr uint32_t REG_TT_ADDR_PTR = 0x310; +constexpr uint32_t REG_TT_DMA_CTRL = 0x314; +constexpr uint32_t REG_TT_INTR_CTRL = 0x318; +constexpr uint32_t REG_TT_DMA_STATUS = 0x31c; +constexpr uint32_t REG_TIMETAGGER_EN = 0x320; +constexpr uint32_t REG_TIMETAGGER_MARK = 0x324; +constexpr uint32_t REG_DIG_SIMULATE = 0x330; +constexpr uint32_t REG_DIG_SAMPLE = 0x338; +constexpr uint32_t REG_LED_STATE = 0x404; + +/* Pending IRQ flags. */ +constexpr uint32_t IRQ_PENDING_ACQ = 0x01; +constexpr uint32_t IRQ_PENDING_TT = 0x02; + +/* DMA status flags. */ +constexpr uint32_t DMA_STATUS_BUSY = 0x01; +constexpr uint32_t DMA_ERROR_READ = 0x02; +constexpr uint32_t DMA_ERROR_WRITE = 0x04; +constexpr uint32_t DMA_ERROR_ADDRESS = 0x08; +constexpr uint32_t DMA_ERROR_ANY = 0x10; + + +/** Synchronize between register access and DMA buffer access. */ +static inline void sync_dma() +{ + asm volatile ("dmb" : : : "memory"); +} + + +/** Trigger modes. */ +enum TriggerMode { + TRIG_NONE = 0, // trigger disabled, manual trigger only + TRIG_AUTO = 1, // continuous triggering + TRIG_EXTERNAL = 2 // external triggering +}; + + +/** Firmware version information. */ +struct VersionInfo { + uint8_t api_version; + uint8_t major_version; + uint8_t minor_version; +}; + + +/** + * Device API for PuzzleFW firmware. + * + * This class uses the UIO driver framework and the custom PuzzleFW Linux + * kernel driver to access the PuzzleFW firmware. + * + * Only a single instance of this class should exist at any time in any + * process on the system. Creating multiple instances, even in separate + * processes, may cause the firmware to function incorrectly. + * + * Methods of this class may throw exceptions to report error conditions. + * "std::invalid_argument" is used to report invalid arguments. + * "std::runtime_error" is used to report errors during opening of the device. + * + * Unless stated otherwise, the methods of this class must not be called + * concurrently from multiple threads without explicit synchronization. + */ +class PuzzleFwDevice +{ +public: + /** Minimum size of the DMA buffer. */ + static constexpr size_t MIN_DMA_BUF_SIZE = 16384; + + /** Maximum number of samples per trigger. */ + static constexpr unsigned int MAX_DECIMATION_FACTOR = 1 << 18; + + /** Maximum number of samples per trigger. */ + static constexpr unsigned int MAX_RECORD_LENGTH = 1 << 16; + + /** Maximum trigger delay. */ + static constexpr unsigned int MAX_TRIGGER_DELAY = (1 << 16) - 1; + + /** Constructor. Opens the device driver. */ + PuzzleFwDevice(); + + // Delete copy constructor and assignment operator. + PuzzleFwDevice(const PuzzleFwDevice&) = delete; + PuzzleFwDevice& operator=(const PuzzleFwDevice&) = delete; + + /** Destructor. Closes the device driver. */ + ~PuzzleFwDevice(); + + /** + * Return the file descriptor of the UIO device node. + * + * It can be used to detect FPGA interrupts. + */ + int uio_fd() const + { + return m_uio_fd; + } + + /** Return a pointer to the DMA buffer. */ + volatile void * dma_buffer() const + { + return m_dma_buf; + } + + /** Return the DMA buffer size in bytes. */ + size_t dma_buffer_size() const + { + return m_dma_buf_size; + } + + /** + * Return the DMA address alignment. + * + * This alignment restriction applies to start address, end address, + * and the limit pointer of DMA stream buffers. + */ + size_t dma_alignment() const + { + return 128; + } + + /** Read from FPGA register. */ + uint32_t read_reg(uint32_t addr) + { + return m_regs[addr / 4]; + } + + /** Write to FPGA register. */ + void write_reg(uint32_t addr, uint32_t value) + { + m_regs[addr / 4] = value; + } + + /** Return firmware version information. */ + VersionInfo get_version_info() + { + uint32_t v = read_reg(REG_INFO); + uint8_t api_version = (v >> 16) & 0xff; + uint8_t major_version = (v >> 8) & 0xff; + uint8_t minor_version = v & 0xff; + return VersionInfo{api_version, major_version, minor_version}; + } + + /** Return the number of analog channels (2 or 4). */ + unsigned int get_analog_channel_count() + { +// TODO -- modify firmware to add ch4-support bit + uint32_t v = read_reg(REG_CH4_MODE); + return (v & 0x100) ? 4 : 2; + } + + /** Return the current value of the timestamp counter. */ + uint64_t get_timestamp() + { + uint32_t vhi, vlo; + vhi = read_reg(REG_TIMESTAMP_HI); + while (1) { + uint32_t vhi_prev = vhi; + vlo = read_reg(REG_TIMESTAMP_LO); + vhi = read_reg(REG_TIMESTAMP_HI); + if (vhi == vhi_prev) { + break; + } + } + return (((uint64_t)vhi) << 32) | vlo; + } + + /** Clear the timestamp counter. */ + void clear_timestamp() + { + write_reg(REG_TIMESTAMP_CLEAR, 1); + } + + /** Return True if the selected LED is on. */ + bool get_led_state(unsigned int led_index) + { + if (led_index > 7) { + throw std::invalid_argument("Invalid led_index"); + } + uint32_t v = read_reg(REG_LED_STATE); + return ((v >> led_index) & 1) != 0; + } + + /** + * Turn the specified LED on or off. + * + * Valid led_index values are 4 to 7. + */ + void set_led_state(unsigned int led_index, bool led_on) + { + if (led_index > 7) { + throw std::invalid_argument("Invalid led_index"); + } + uint32_t v = read_reg(REG_LED_STATE); + uint32_t m = (1 << led_index); + if (led_on) { + v |= m; + } else { + v &= ~m; + } + write_reg(REG_LED_STATE, v); + } + + /** Return the current trigger mode. */ + TriggerMode get_trigger_mode() + { + uint32_t v = read_reg(REG_TRIGGER_MODE); + if ((v & 0x01) != 0) { + return TRIG_AUTO; + } + if ((v & 0x02) != 0) { + return TRIG_EXTERNAL; + } + return TRIG_NONE; + } + + /** Set trigger mode. */ + void set_trigger_mode(TriggerMode mode) + { + uint32_t v = read_reg(REG_TRIGGER_MODE); + v &= 0xfc; + if (mode == TRIG_AUTO) { + v |= 0x01; + } + if (mode == TRIG_EXTERNAL) { + v |= 0x02; + } + write_reg(REG_TRIGGER_MODE, v); + } + + /** Return the selected external trigger channel. */ + unsigned int get_trigger_ext_channel() + { + uint32_t v = read_reg(REG_TRIGGER_MODE); + return ((v >> 4) & 7); + } + + /** Select external trigger channel. */ + void set_trigger_ext_channel(unsigned int channel) + { + if (channel > 7) { + throw std::invalid_argument("Invalid trigger channel"); + } + uint32_t v = read_reg(REG_TRIGGER_MODE); + v &= 0x8f; + v |= (channel << 4); + write_reg(REG_TRIGGER_MODE, v); + } + + /** Return true if triggering on falling edge. */ + bool get_trigger_ext_falling() + { + uint32_t v = read_reg(REG_TRIGGER_MODE); + return ((v & 0x80) != 0); + } + + /** Select active edge for external trigger signal. */ + void set_trigger_ext_falling(bool falling) + { + uint32_t v = read_reg(REG_TRIGGER_MODE); + v &= 0x7f; + if (falling) { + v |= 0x80; + } + write_reg(REG_TRIGGER_MODE, v); + } + + /** Force a single trigger event. */ + void trigger_force() + { + uint32_t v = read_reg(REG_TRIGGER_MODE); + write_reg(REG_TRIGGER_MODE, v | 0x100); + } + + /** Return configured trigger delay (in ADC samples). */ + unsigned int get_trigger_delay() + { + return read_reg(REG_TRIGGER_DELAY); + } + + /** Set trigger delay (in ADC samples). */ + void set_trigger_delay(unsigned int delay) + { + if (delay > MAX_TRIGGER_DELAY) { + throw std::invalid_argument("Invalid trigger delay"); + } + write_reg(REG_TRIGGER_DELAY, delay); + } + + /** Return true if analog acquisition is waiting for a trigger. */ + bool is_waiting_for_trigger() + { + uint32_t v = read_reg(REG_TRIGGER_STATUS); + return ((v & 1) != 0); + } + + /** Return true if analog acquisition is enabled. */ + bool is_acquisition_enabled() + { + uint32_t v = read_reg(REG_ACQUISITION_EN); + return ((v & 1) != 0); + } + + /** Enable or disable analog acquisition. */ + void set_acquisition_enabled(bool enable) + { + write_reg(REG_ACQUISITION_EN, enable ? 1 : 0); + } + + /** Return true if 4-channel mode is enabled. */ + bool is_4channel_mode() + { + uint32_t v = read_reg(REG_CH4_MODE); + return ((v & 1) != 0); + } + + /** Enable or disable 4-channel mode. */ + void set_4channel_mode(bool enable) + { + write_reg(REG_CH4_MODE, enable ? 1 : 0); + } + + /** Return the decimation factor. */ + unsigned int get_decimation_factor() + { + uint32_t v = read_reg(REG_DECIMATION_FACTOR); + return v + 1; + } + + /** + * Set the decimation factor (number of ADC samples per decimated sample). + * + * Valid values are 1 to MAX_DECIMATION_FACTOR. + * This method writes the specified value minus 1 to the FPGA register. + */ + void set_decimation_factor(unsigned int decimation_factor) + { + if (decimation_factor == 0 + || decimation_factor > MAX_DECIMATION_FACTOR) { + throw std::invalid_argument("Invalid decimation factor"); + } + write_reg(REG_DECIMATION_FACTOR, decimation_factor - 1); + } + + /** Return true if sample averaging is enabled. */ + bool is_averaging_enabled() + { + uint32_t v = read_reg(REG_AVERAGING_EN); + return ((v & 1) != 0); + } + + /** Enable or disable sample averaging. */ + void set_averaging_enabled(bool enable) + { + write_reg(REG_AVERAGING_EN, enable ? 1 : 0); + } + + /** Return the number of shift-right positions for decimated samples. */ + int get_shift_steps() + { + return read_reg(REG_SHIFT_STEPS); + } + + /** + * Set the number of shift-right positions for decimated samples. + * + * Valid values are 0 to 8 steps. + */ + void set_shift_steps(int shift_steps) + { + if (shift_steps < 0 || shift_steps > 8) { + throw std::invalid_argument("Invalid shift_steps"); + } + write_reg(REG_SHIFT_STEPS, shift_steps); + } + + /** Return the record length (number of samples per trigger). */ + unsigned int get_record_length() + { + return read_reg(REG_RECORD_LENGTH) + 1; + } + + /** + * Set record length (number of samples per trigger). + * + * Valid values are 1 to MAX_RECORD_LENGTH. + * This method writes the specified value minus 1 to the FPGA register. + */ + void set_record_length(unsigned int samples) + { + if (samples == 0 || samples > MAX_RECORD_LENGTH) { + throw std::invalid_argument("Invalid record length"); + } + write_reg(REG_RECORD_LENGTH, samples - 1); + } + + /** Return the most recent ADC sample for the specified channel. */ + unsigned int get_adc_sample(unsigned int channel) + { + if (channel >= 4) { + throw std::invalid_argument("Invalid ADC channel"); + } + uint32_t addr = (channel < 2) ? REG_ADC_SAMPLE : REG_ADC23_SAMPLE; + uint32_t v = read_reg(addr); + return (v >> (16 * (channel & 1))) & 0x3fff; + } + + /** Return minimum and maximum sample value sinc last cleared. */ + void get_adc_range(unsigned int channel, + unsigned int& min_sample, + unsigned int& max_sample) + { + if (channel >= 4) { + throw std::invalid_argument("Invalid ADC channel"); + } + uint32_t addr = (channel == 0) ? REG_ADC0_MINMAX : + (channel == 1) ? REG_ADC1_MINMAX : + (channel == 2) ? REG_ADC2_MINMAX : + REG_ADC3_MINMAX; + uint32_t v = read_reg(addr); + min_sample = v & 0x3fff; + max_sample = (v >> 16) & 0x3fff; + } + + /** Clear minimum/maximum sample ranges. */ + void clear_adc_range() + { + write_reg(REG_ADC_RANGE_CLEAR, 1); + } + + /** Return true if ADC sample simulation is enabled. */ + bool is_adc_simulation_enabled() + { + uint32_t v = read_reg(REG_SIMULATE_ADC); + return ((v & 1) != 0); + } + + /** Enable or disable ADC sample simulation. */ + void set_adc_simulation_enabled(bool enable) + { + write_reg(REG_SIMULATE_ADC, enable ? 1 : 0); + } + + /** Return the mask of enabled timetagger event types. */ + uint32_t get_timetagger_event_mask() + { + return read_reg(REG_TIMETAGGER_EN); + } + + /** + * Set mask of enabled timetagger event types. + * + * Bit 0 enables rising edges on digital input channel 0. + * Bit 1 enables falling edges on digital input channel 0. + * Bit 2 enables rising edges on digital input channel 1. + * And so on. + */ + void set_timetagger_event_mask(uint32_t event_mask) + { + write_reg(REG_TIMETAGGER_EN, event_mask); + } + + /** Request a marker record in the timetagger event stream. */ + void timetagger_mark() + { + write_reg(REG_TIMETAGGER_MARK, 1); + } + + /** Return a bitmask representing the current digital input state. */ + uint32_t get_digital_input_state() + { + return read_reg(REG_DIG_SAMPLE); + } + + /** Return true if digital signal simulation is enabled. */ + bool is_digital_simulation_enabled() + { + uint32_t v = read_reg(REG_DIG_SIMULATE); + return ((v & 0x100) != 0); + } + + /** Enable or disable digital signal simulation. */ + void set_digital_simulation_enabled(bool enable) + { + uint32_t v = read_reg(REG_DIG_SIMULATE); + if (enable) { + v |= 0x100; + } else { + v &= 0xff; + } + write_reg(REG_DIG_SIMULATE, v); + } + + /** Return simulated digital input state. */ + uint32_t get_digital_simulation_state() + { + uint32_t v = read_reg(REG_DIG_SIMULATE); + return v & 0xff; + } + + /** Set simulated digital input state. */ + void set_digital_simulation_state(uint32_t mask) + { + uint32_t v = read_reg(REG_DIG_SIMULATE); + v &= 0x100; + v |= mask; + write_reg(REG_DIG_SIMULATE, v); + } + + /** Return true if FPGA interrupts are enabled. */ + bool is_irq_enabled() + { + uint32_t v = read_reg(REG_IRQ_ENABLE); + return ((v & 1) != 0); + } + + /** + * Enable FPGA interrupts. + * + * The Linux kernel driver will automatically disable interrupts + * whenever an interrupt occurs. + * + * It is safe to call this function concurrently from multiple threads. + */ + void enable_irq() + { + write_reg(REG_IRQ_ENABLE, 1); + } + + /** + * Return bit mask of pending interrupt conditions. + * + * It is safe to call this function concurrently from multiple threads. + */ + uint32_t get_irq_pending() + { + return read_reg(REG_IRQ_PENDING); + } + + /** Return true if the FPGA DMA engine is enabled. */ + bool is_dma_enabled() + { + uint32_t v = read_reg(REG_DMA_EN); + return ((v & 1) != 0); + } + + /** Enable or disable the FPGA DMA engine. */ + void set_dma_enabled(bool enable) + { + write_reg(REG_DMA_EN, enable ? 1 : 0); + } + + /** Return bit mask of DMA status and error flags. */ + uint32_t get_dma_status() + { + return read_reg(REG_DMA_STATUS); + } + + /** + * Clear pending DMA error flags. + * + * Before doing this, any active DMA data streams should be stopped. + */ + void clear_dma_errors() + { + write_reg(REG_DMA_CLEAR, 1); + } + +private: + /** File descriptor of the UIO device. */ + int m_uio_fd; + + /** Pointer to device registers mapped in process address space. */ + volatile uint32_t * m_regs; + + /** Pointer to DMA buffer mapped in process address space. */ + volatile void * m_dma_buf; + + /** Size of DMA buffer (in bytes). */ + size_t m_dma_buf_size; + + /** Full name of the PuzzleFW platform device. */ + std::string m_device_name; + + /** Path name of UIO device node. */ + std::string m_uio_path; +}; + + +/** + * Manage a DMA data write stream. + * + * The PuzzleFW firmware provides two DMA write streams: one stream for + * analog sample data and one stream for timetagger data. + * + * Only a single instance of this class should exist for a specific + * DMA stream type at any time. I.e. at most one instance should exist + * for analog sample data, and at most one instance for timetagger data. + * + * Each DMA stream uses a segment within the global DMA buffer. + * Users of this class must make sure that the stream buffer segment + * is located within the global DMA buffer, and that it does not overlap + * with any buffer segment for another DMA stream. + * + * An instance of this class must not be accessed concurrently from + * multiple threads without explicit synchronization. However, multiple + * threads may safely access different instances of this class. + */ +class DmaWriteStream +{ +public: + enum StreamType { DMA_ACQ = 1, DMA_TT }; + + /** Keep this number of bytes unused in the DMA stream buffer segment. */ + static constexpr size_t POINTER_MARGIN = 4096; + + /** + * Initialize DMA stream. + * + * The DMA stream is initially disabled. + * + * Parameters "buf_start" and "buf_end" specify the start and end offset + * of the buffer segment, relative to the start of the global DMA buffer. + * These must be aligned to the DMA address alignment as returned by + * "PuzzleFwDevice::dma_alignment()". + * + * The DMA stream instance keeps an internal pointer to the device + * instance. The device instance must remain valid for the life time + * of the DMA stream instance. + */ + DmaWriteStream(PuzzleFwDevice& device, + StreamType stream_type, + size_t buf_start, + size_t buf_end) + : m_device(device) + , m_reg((stream_type == DMA_ACQ) ? regs_acq : regs_tt) + , m_irq_pending_mask((stream_type == DMA_ACQ) ? + IRQ_PENDING_ACQ : IRQ_PENDING_TT) + , m_buf_start(buf_start) + , m_buf_end(buf_end) + , m_read_pointer(buf_start) + { + if (buf_start >= buf_end + || buf_end - buf_start < 4 * POINTER_MARGIN + || buf_end > m_device.dma_buffer_size() + || buf_start % m_device.dma_alignment() != 0 + || buf_end % m_device.dma_alignment() != 0) { + throw std::invalid_argument("Invalid buffer segment"); + } + + // Disable DMA stream. + m_device.write_reg(m_reg.dma_ctrl, 0); + + // Disable DMA stream interrupts and clear pending interrupts. + disable_interrupt(); + + // Set up DMA buffer segment. + m_device.write_reg(m_reg.addr_start, m_buf_start); + m_device.write_reg(m_reg.addr_end, m_buf_end); + m_device.write_reg(m_reg.addr_limit, m_buf_end - POINTER_MARGIN); + + // Initialize DMA stream (reset write pointer to start of segment). + m_device.write_reg(m_reg.dma_ctrl, 2); + } + + // Delete copy constructor and assignment operator. + DmaWriteStream(const DmaWriteStream&) = delete; + DmaWriteStream& operator=(const DmaWriteStream&) = delete; + + /** Disable DMA stream. Remaining data in buffer is discarded. */ + ~DmaWriteStream() + { + // Disable DMA stream and re-initialize. + m_device.write_reg(m_reg.dma_ctrl, 0); + m_device.write_reg(m_reg.dma_ctrl, 2); + + // Disable DMA stream interrupt and clear pending interrupt. + disable_interrupt(); + } + + /** Return the size of the DMA buffer segment. */ + size_t buffer_segment_size() const + { + return m_buf_end - m_buf_start; + } + + /** Return the DMA address alignment. */ + size_t dma_alignment() const + { + return m_device.dma_alignment(); + } + + /** Enable or disable the DMA stream. */ + void set_enabled(bool enable) + { + m_device.write_reg(m_reg.dma_ctrl, enable ? 1 : 0); + } + + /** Return the number of bytes available in the DMA buffer. */ + size_t get_data_available() + { + uint32_t write_pointer = m_device.read_reg(m_reg.addr_ptr); + if (write_pointer >= m_read_pointer) { + return write_pointer - m_read_pointer; + } else { + return m_buf_end - m_buf_start + write_pointer - m_read_pointer; + } + } + + /** + * Return a contiguous block of available data in the DMA buffer. + * + * This function may return a subset of the available data. + */ + void get_data_block(void *& ptr, size_t& n) + { + uint32_t write_pointer = m_device.read_reg(m_reg.addr_ptr); + + // Ensure that upcoming memory access to the buffer occurs AFTER + // reading the pointer register. + sync_dma(); + + ptr = (char *)(m_device.dma_buffer()) + m_read_pointer; + if (write_pointer >= m_read_pointer) { + n = write_pointer - m_read_pointer; + } else { + n = m_buf_end - m_read_pointer; + } + } + + /** + * Release the specified number of bytes from the buffer. + * + * Update the read pointer and allow the DMA engine to reuse + * the released range for new data. + * + * The specified size must be a multiple of 8 bytes. + */ + void consume_data(size_t n) + { + if ((n & 7) != 0) { + throw std::invalid_argument("Invalid size"); + } + + // Update read pointer. + if (n < m_buf_end - m_read_pointer) { + m_read_pointer += n; + } else { + m_read_pointer = m_read_pointer + n - m_buf_end + m_buf_start; + } + + // Update limit address. + // Round down to aligned address. + uint32_t limit; + if (m_read_pointer - m_buf_start < POINTER_MARGIN) { + limit = m_read_pointer - m_buf_start + m_buf_end - POINTER_MARGIN; + } else { + limit = m_read_pointer - POINTER_MARGIN; + } + limit &= ~((uint32_t)127); + + sync_dma(); + + m_device.write_reg(m_reg.addr_limit, limit); + } + + /** Return true if the interrupt for this DMA stream is pending. */ + bool is_interrupt_pending() + { + uint32_t irq_pending = m_device.get_irq_pending(); + return (irq_pending & m_irq_pending_mask) != 0; + } + + /** Enable interrupt when specified amount of data is available. */ + void enable_interrupt_on_data_available(size_t n) + { + // Update interrupt pointer. + uint32_t threshold; + if (m_buf_end - m_read_pointer > n) { + threshold = m_read_pointer + n; + } else { + threshold = m_read_pointer + n - m_buf_end + m_buf_start; + } + m_device.write_reg(m_reg.addr_intr, threshold); + + // Enable interrupt. + m_device.write_reg(m_reg.intr_ctrl, 1); + } + + /** Disable interrupt and clear pending interrupt for this DMA stream. */ + void disable_interrupt() + { + m_device.write_reg(m_reg.intr_ctrl, 2); + } + +private: + struct RegisterAddress { + uint32_t addr_start; + uint32_t addr_end; + uint32_t addr_limit; + uint32_t addr_intr; + uint32_t addr_ptr; + uint32_t dma_ctrl; + uint32_t intr_ctrl; + uint32_t dma_status; + }; + + static constexpr RegisterAddress regs_acq { + REG_ACQ_ADDR_START, + REG_ACQ_ADDR_END, + REG_ACQ_ADDR_LIMIT, + REG_ACQ_ADDR_INTR, + REG_ACQ_ADDR_PTR, + REG_ACQ_DMA_CTRL, + REG_ACQ_INTR_CTRL, + REG_ACQ_DMA_STATUS + }; + + static constexpr RegisterAddress regs_tt { + REG_TT_ADDR_START, + REG_TT_ADDR_END, + REG_TT_ADDR_LIMIT, + REG_TT_ADDR_INTR, + REG_TT_ADDR_PTR, + REG_TT_DMA_CTRL, + REG_TT_INTR_CTRL, + REG_TT_DMA_STATUS + }; + + PuzzleFwDevice& m_device; + const RegisterAddress m_reg; + const uint32_t m_irq_pending_mask; + const uint32_t m_buf_start; + const uint32_t m_buf_end; + uint32_t m_read_pointer; +}; + + +}; // namespace puzzlefw + +#endif // PUZZLEFW_H_