Add C++ software

Not tested yet.
This commit is contained in:
Joris van Rantwijk 2024-09-18 19:48:34 +02:00
parent 6a77330407
commit 674229791f
7 changed files with 2374 additions and 2 deletions

View File

@ -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

View File

@ -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_

View File

@ -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_

View File

@ -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_

View File

@ -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 */

View File

@ -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);
}

View File

@ -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_