parent
6a77330407
commit
674229791f
|
@ -3,14 +3,22 @@
|
||||||
#
|
#
|
||||||
|
|
||||||
CC = $(CROSS_COMPILE)gcc
|
CC = $(CROSS_COMPILE)gcc
|
||||||
|
CXX = $(CROSS_COMPILE)g++
|
||||||
CFLAGS = -Wall -O2
|
CFLAGS = -Wall -O2
|
||||||
|
CXXFLAGS = -std=c++17 -Wall -O2
|
||||||
|
|
||||||
.PHONY: all
|
.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
|
testje: testje.c
|
||||||
|
|
||||||
.PHONY: clean
|
.PHONY: clean
|
||||||
clean:
|
clean:
|
||||||
$(RM) -f testje
|
$(RM) -f -- puzzlecmd testje *.o
|
||||||
|
|
||||||
|
|
|
@ -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 <assert.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <chrono>
|
||||||
|
#include <iomanip>
|
||||||
|
#include <stdexcept>
|
||||||
|
#include <system_error>
|
||||||
|
|
||||||
|
#include <boost/asio.hpp>
|
||||||
|
#include <boost/system/error_code.hpp>
|
||||||
|
|
||||||
|
#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<asio::io_context::executor_type> 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_
|
|
@ -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 <initializer_list>
|
||||||
|
#include <system_error>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include <boost/asio.hpp>
|
||||||
|
#include <boost/system/error_code.hpp>
|
||||||
|
|
||||||
|
#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 <typename Func>
|
||||||
|
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<std::function<void()>> m_callbacks;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
} // namespace puzzlefw
|
||||||
|
|
||||||
|
#endif // PUZZLEFW_INTERRUPT_MANAGER_H_
|
|
@ -0,0 +1,59 @@
|
||||||
|
/*
|
||||||
|
* logging.hpp
|
||||||
|
*
|
||||||
|
* Log messages to stdout.
|
||||||
|
*
|
||||||
|
* Joris van Rantwijk 2024
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef PUZZLEFW_LOGGING_H_
|
||||||
|
#define PUZZLEFW_LOGGING_H_
|
||||||
|
|
||||||
|
#include <stdarg.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <time.h>
|
||||||
|
|
||||||
|
|
||||||
|
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_
|
|
@ -0,0 +1,599 @@
|
||||||
|
/*
|
||||||
|
* puzzlecmd.cpp
|
||||||
|
*
|
||||||
|
* Command-line program to test PuzzleFW firmware.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <limits.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <chrono>
|
||||||
|
#include <optional>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
#include <getopt.h>
|
||||||
|
#include <boost/asio.hpp>
|
||||||
|
|
||||||
|
#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<bool> acquisition_en;
|
||||||
|
std::optional<int> channels;
|
||||||
|
std::optional<puzzlefw::TriggerMode> trigger_mode;
|
||||||
|
std::optional<int> trigger_channel;
|
||||||
|
std::optional<bool> trigger_falling;
|
||||||
|
std::optional<int> trigger_delay;
|
||||||
|
bool trigger_force;
|
||||||
|
std::optional<int> record_len;
|
||||||
|
std::optional<int> divisor;
|
||||||
|
std::optional<bool> averaging_en;
|
||||||
|
std::optional<int> shift_steps;
|
||||||
|
std::optional<unsigned int> timetagger_channels;
|
||||||
|
bool marker;
|
||||||
|
std::optional<bool> adc_sim;
|
||||||
|
std::optional<int> 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 */
|
|
@ -0,0 +1,171 @@
|
||||||
|
/*
|
||||||
|
* puzzlefw.cpp
|
||||||
|
*
|
||||||
|
* C++ library for PuzzleFW firmware.
|
||||||
|
*
|
||||||
|
* Joris van Rantwijk 2024
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <errno.h>
|
||||||
|
#include <fcntl.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
#include <sys/mman.h>
|
||||||
|
#include <filesystem>
|
||||||
|
#include <fstream>
|
||||||
|
#include <stdexcept>
|
||||||
|
#include <system_error>
|
||||||
|
|
||||||
|
#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);
|
||||||
|
}
|
|
@ -0,0 +1,899 @@
|
||||||
|
/*
|
||||||
|
* puzzlefw.hpp
|
||||||
|
*
|
||||||
|
* C++ library for PuzzleFW firmware.
|
||||||
|
*
|
||||||
|
* Joris van Rantwijk 2024
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef PUZZLEFW_H_
|
||||||
|
#define PUZZLEFW_H_
|
||||||
|
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <stdexcept>
|
||||||
|
|
||||||
|
|
||||||
|
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_
|
Loading…
Reference in New Issue