added SPI communication

This commit is contained in:
2026-05-29 15:55:12 +02:00
parent 8f18eaca92
commit 7d8a7ade00
10 changed files with 549 additions and 29 deletions
@@ -2,6 +2,7 @@
set(MATTERHORN_SOURCES
${CMAKE_CURRENT_SOURCE_DIR}/src/MatterhornApp.cpp
${CMAKE_CURRENT_SOURCE_DIR}/src/SPICommunication.cpp
)
if(SLS_USE_SIMULATOR)
@@ -3,6 +3,7 @@
#include "DetectorServer.h"
#include "MemoryModel.hpp"
#include "RegisterDefs.hpp"
#include "SPICommunication.h"
#include "SpecializedTemplates.h"
#include "TCPInterface.h"
#include "fmt/format.h"
@@ -55,6 +56,10 @@ class BaseMatterhornServer
ReturnCode set_module_position(ServerInterface &socket);
ReturnCode set_counter_mask(ServerInterface &socket);
ReturnCode get_counter_mask(ServerInterface &socket) const;
uint64_t getNumFrames() const;
void setNumFrames(const uint64_t num_frames);
@@ -75,12 +80,19 @@ class BaseMatterhornServer
protected:
using MemoryModel = std::conditional_t<
std::is_same_v<DerivedServer, VirtualMatterhornServer>,
VirtualMemoryModel, HardwareMemoryModel>;
VirtualMemoryModel<uint32_t>, HardwareMemoryModel>; // 32 bit registers
// TODO: for now in MatterhornServer and not generic Server but can be
// templated on different IPCore types for each detector
BusCommunication<IPCore, MemoryModel> busCommunication{};
using SPICommunicationClass = std::conditional_t<
std::is_same_v<DerivedServer, VirtualMatterhornServer>,
SPICommunication<VirtualSPICommunication<MatterhornSPIRegisters>>,
SPICommunication<HardwareSPICommunication>>;
SPICommunicationClass spiCommunication{};
// TODO: probably virtaul server specific details, can be moved to derived
// class
/// @brief initial setup of detector
@@ -99,6 +111,10 @@ BaseMatterhornServer<DerivedServer>::processFunction(const detFuncs function_id,
ServerInterface &socket) {
switch (function_id) {
case detFuncs::F_SET_COUNTER_MASK:
return set_counter_mask(socket);
case detFuncs::F_GET_COUNTER_MASK:
return get_counter_mask(socket);
default:
throw RuntimeError(
fmt::format("Function {} not implemented",
@@ -314,4 +330,61 @@ ReturnCode BaseMatterhornServer<DerivedServer>::set_module_position(
return static_cast<ReturnCode>(socket.Send(ReturnCode::OK));
}
template <typename DerivedServer>
ReturnCode
BaseMatterhornServer<DerivedServer>::set_counter_mask(ServerInterface &socket) {
// TODO: update properly
uint32_t counter_mask{};
try {
int ret = socket.Receive(counter_mask);
} catch (const SocketError &e) {
LOG(logERROR) << "Failed to receive counter mask: " << e.what();
return ReturnCode::FAIL;
}
try {
auto reg_value = spiCommunication.SPIread(
SPIRegisters::NUM_COUNTERS.register_,
0); // TODO: how to handle different chip ids -> e.g. broadcast do
// we want it to be configurable for different chip ids? -
// Command overload for some of the SPI registers
setSPIRegisterField(reg_value, SPIRegisters::NUM_COUNTERS,
counter_mask);
spiCommunication.SPIwrite(SPIRegisters::NUM_COUNTERS.register_, 0,
reg_value);
} catch (const std::exception &e) {
LOG(logERROR) << "Failed to set counter mask: " << e.what();
return ReturnCode::FAIL;
}
return static_cast<ReturnCode>(socket.Send(ReturnCode::OK));
}
template <typename DerivedServer>
ReturnCode BaseMatterhornServer<DerivedServer>::get_counter_mask(
ServerInterface &socket) const {
// TODO: update properly
std::vector<std::byte> reg_value{};
try {
reg_value = spiCommunication.SPIread(
SPIRegisters::NUM_COUNTERS.register_,
0); // TODO: how to handle different chip ids -
} catch (const std::exception &e) {
LOG(logERROR) << "Failed to read counter mask from SPI register: "
<< e.what();
return ReturnCode::FAIL;
}
uint32_t counter_mask =
getSPIRegisterField(reg_value, SPIRegisters::NUM_COUNTERS);
return static_cast<ReturnCode>(socket.sendResult(counter_mask));
}
} // namespace sls
@@ -0,0 +1,30 @@
#pragma once
#include <cstdint>
namespace sls {
namespace MatterhornDefs {
constexpr uint8_t NUM_CHIPS_PER_MODULE = 8;
struct MatterhornSPIRegisters {
constexpr static std::array<SPIRegister, 10> spiregisters{
SPIRegisters::SPI_REG_ConfigCML,
SPIRegisters::SPI_REG_ManualSelector,
SPIRegisters::SPI_REG_CoreRSTUnit,
SPIRegisters::SPI_REG_StoreRSTUnit,
SPIRegisters::SPI_REG_Trimbits,
SPIRegisters::SPI_REG_McGyver,
SPIRegisters::SPI_REG_McGyver_par_load,
SPIRegisters::SPI_REG_ActionReg,
SPIRegisters::SPI_REG_InternalDACs,
SPIRegisters::SPI_REG_ChecksumReg};
constexpr static uint8_t NUM_CHIPS_PER_MODULE =
MatterhornDefs::NUM_CHIPS_PER_MODULE;
};
} // namespace MatterhornDefs
} // namespace sls
@@ -0,0 +1,172 @@
#include "MemoryModel.hpp"
#include "SPIRegisterDefs.hpp"
#include "SPIRegisterHelperStructs.hpp"
#include "fmt/format.h"
#include "sls/logger.h"
#include <map>
#include <vector>
namespace sls {
/// @brief abstract base class for SPI communication
template <typename DerivedSPIModel> class SPICommunication {
public:
SPICommunication();
~SPICommunication();
std::vector<std::byte> SPIread(const SPIRegister &spi_reg,
const uint8_t chip_id) const;
void SPIwrite(const SPIRegister &spi_reg, const uint8_t chip_id,
const std::vector<std::byte> &data);
};
template <typename DerivedSPIModel>
SPICommunication<DerivedSPIModel>::SPICommunication() {
static_cast<DerivedSPIModel *>(this)->map_to_memory();
}
template <typename DerivedSPIModel>
SPICommunication<DerivedSPIModel>::~SPICommunication() {
static_cast<DerivedSPIModel *>(this)->unmap_memory();
}
template <typename DerivedSPIModel>
std::vector<std::byte>
SPICommunication<DerivedSPIModel>::SPIread(const SPIRegister &spi_reg,
const uint8_t chip_id) const {
return static_cast<const DerivedSPIModel *>(this)->spi_read(
spi_reg.n_bytes, chip_id, spi_reg.spi_register_id);
}
template <typename DerivedSPIModel>
void SPICommunication<DerivedSPIModel>::SPIwrite(
const SPIRegister &spi_reg, const uint8_t chip_id,
const std::vector<std::byte> &data) {
if (data.size() != spi_reg.n_bytes) {
LOG(logERROR) << fmt::format("Data size {} does not match number of "
"bytes {} for SPI register {}",
data.size(), spi_reg.n_bytes,
spi_reg.spi_register_id);
throw std::runtime_error(
fmt::format("Data size {} does not match number of bytes {} for "
"SPI register {}",
data.size(), spi_reg.n_bytes, spi_reg.spi_register_id));
}
static_cast<DerivedSPIModel *>(this)->spi_write(
chip_id, spi_reg.spi_register_id, data);
static_cast<DerivedSPIModel *>(this)->spi_write(
chip_id, SPIRegisters::SPI_REG_ExtraClocks.spi_register_id,
std::vector<std::byte>{
std::byte{0x00}}); // extra clock trigger to actually load the
// new value into the register
}
/**
* Non destructive read from SPI register. Read n_bytes by shifting in
* dummy data while keeping csn 0 after the operation. Shift the read out
* data back in to restore the register.
*/
class HardwareSPICommunication
: public SPICommunication<HardwareSPICommunication> {
public:
void map_to_memory();
void unmap_memory();
void spi_write(const uint8_t chip_id, const uint8_t register_id,
const std::vector<std::byte> &data);
std::vector<std::byte> spi_read(const size_t n_bytes, const uint8_t chip_id,
const uint8_t register_id) const;
private:
int spi_filedescriptor = -1;
};
// template <SPIRegister... SPIRegisters, uint8_t NUM_CHIPS_PER_MODULE> // non
// type template parameters only for c++20
template <typename SPIRegisters> // TODO add a type trait to ensure it stores
// all fields
class VirtualSPICommunication
: public SPICommunication<VirtualSPICommunication<SPIRegisters>> {
public:
VirtualSPICommunication() {
// TODO should it be in the constructor?
/*
(virtual_registers.emplace(
SPIRegisters.spi_register_id,
VirtualMemoryModel<std::byte>{SPIRegisters.spi_register_id,
SPIRegisters.n_bytes *
NUM_CHIPS_PER_MODULE}),
...);
*/
for (const auto &reg : SPIRegisters::spiregisters) {
virtual_registers.emplace(
reg.spi_register_id,
VirtualMemoryModel<std::byte>{
reg.spi_register_id,
reg.n_bytes * SPIRegisters::NUM_CHIPS_PER_MODULE});
}
}
void map_to_memory() {
// resize the virtual register memory to the correct size based on
// the defined SPI registers
for (auto &[register_id, register_memory] : virtual_registers) {
register_memory.mapToMemory();
}
}
void unmap_memory() { // do nothing
}
std::vector<std::byte> spi_read(const size_t n_bytes, const uint8_t chip_id,
const uint8_t register_id) const {
auto mapped_register =
virtual_registers.at(register_id).getMappedMemoryPtr();
mapped_register +=
chip_id * n_bytes; // TODO: how to handle different chip ids ->
// e.g. broadcast do we want it to be
// configurable for different chip ids?
// TODO: should I emulate the shifting in of dummy data and shifting
// out of the register data here to be more realistic?
std::vector<std::byte> output_data(n_bytes);
std::memcpy(output_data.data(), mapped_register, n_bytes);
return output_data;
}
void spi_write(const uint8_t chip_id, const uint8_t register_id,
const std::vector<std::byte> &data) {
auto mapped_register =
virtual_registers.at(register_id).getMappedMemoryPtr();
mapped_register +=
chip_id * data.size(); // TODO: how to handle different
// chip ids -> e.g. broadcast do
// TODO: should I emulate the shifting in of dummy data and shifting
// out of
std::memcpy(mapped_register, data.data(), data.size());
}
private:
/// @brief map of register id to virtual memory model for each register
std::map<uint16_t, VirtualMemoryModel<std::byte>> virtual_registers{};
};
} // namespace sls
@@ -0,0 +1,51 @@
#pragma once
#include "SPIRegisterHelperStructs.hpp"
#include <cstdint>
namespace sls {
namespace SPIRegisters {
// SPI registers
constexpr SPIRegister SPI_REG_ConfigUnit{0, 8};
constexpr SPIRegister SPI_REG_ConfigCML{1, 1};
constexpr SPIRegister SPI_REG_ManualSelector{2, 2};
constexpr SPIRegister SPI_REG_CoreRSTUnit{3, 4};
constexpr SPIRegister SPI_REG_StoreRSTUnit{4, 2};
constexpr SPIRegister SPI_REG_Trimbits{5, 256};
constexpr SPIRegister SPI_REG_McGyver{6, 512};
constexpr SPIRegister SPI_REG_McGyver_par_load{7, 512};
constexpr SPIRegister SPI_REG_ActionReg{11, 1};
constexpr SPIRegister SPI_REG_InternalDACs{13, 4};
constexpr SPIRegister SPI_REG_ChecksumReg{14, 32};
// Used to generate extra clocks after writing to trigger the load of the new
// value
constexpr SPIRegister SPI_REG_ExtraClocks{12,
1}; // TODO: dont know what size is
// SPI register fields
constexpr SPIRegisterField OUTPUT_MODE{SPI_REG_ConfigUnit, 4, 3};
/// @brief first two bits starting counter, second two bits number of counters
/// to read e.g. 0001 -> read counter 0 and 1, 0100 -> read counter 1
constexpr SPIRegisterField NUM_COUNTERS{SPI_REG_ConfigUnit, 8, 4};
/// @brief 00-> 16 bit, 01 -> 8 bit, 10 -> 4 bit, 11 -> reserved 16 bit
constexpr SPIRegisterField DYNAMIC_RANGE{SPI_REG_ConfigUnit, 14, 2};
// TODO: continue defining the rest of the fields as needed
} // namespace SPIRegisters
} // namespace sls
@@ -0,0 +1,84 @@
#pragma once
#include "fmt/format.h"
#include <cstdint>
#include <vector>
namespace sls {
struct SPIRegister {
/// @brief SPI register ID (0-15)
uint16_t spi_register_id{};
/// @brief number of bytes in the register
uint64_t n_bytes{};
};
struct SPIRegisterField {
/// @brief SPI register to which teh field belongs
SPIRegister register_{};
/// @brief least significant bit position of the field in the register
uint64_t lsb_position{};
/// @brief number of bits in the field
/// TODO: can it be larger?
uint32_t num_bits{};
};
// TODO: maybe change uint32_t but max field size is 32 bits so should be fine
// for now
void inline setSPIRegisterField(std::vector<std::byte> &register_value,
const SPIRegisterField &field,
uint32_t field_value) {
// check that the field value can fit in the bitmask
if (field_value > field.num_bits) {
throw std::invalid_argument(fmt::format(
"Value {} cannot fit in field {}", field_value, field.num_bits));
}
constexpr uint8_t bits_per_byte = 8;
// TODO: mmh doesnt feel very modern - maybe better to cast to uint32_t,
// alignment issues?
for (std::size_t i = 0; i < field.num_bits; ++i) {
std::size_t offset = field.lsb_position + i;
std::size_t byte_index = offset / bits_per_byte;
std::size_t bit_index = offset % bits_per_byte;
std::byte mask = std::byte(1) << bit_index;
register_value[byte_index] &=
~mask; // clear the bit in the register value
register_value[byte_index] |= std::byte(
((field_value >> i) & 0x1)
<< bit_index); // set the new field value bit in the register value
}
}
uint32_t inline getSPIRegisterField(
const std::vector<std::byte> &register_value,
const SPIRegisterField &field) {
uint32_t field_value = 0;
constexpr uint8_t bits_per_byte = 8;
for (std::size_t i = 0; i < field.num_bits; ++i) {
std::size_t offset = field.lsb_position + i;
std::size_t byte_index = offset / bits_per_byte;
std::size_t bit_index = offset % bits_per_byte;
std::byte mask = std::byte(1) << bit_index;
field_value |=
(static_cast<uint32_t>((register_value[byte_index] & mask) >>
bit_index)
<< i); // extract the field value bit from the register value and
// set it in the correct position in the field value
}
return field_value;
}
} // namespace sls
@@ -1,5 +1,6 @@
#pragma once
#include "ArmBusCommunication.hpp"
#include "MemoryModel.hpp"
#include "RegisterDefs.hpp"
/**
@@ -7,7 +8,6 @@
* @short contains specializations of the template classes for the Matterhorn
* server implementation
*/
namespace sls {
template <typename MemoryModel>
@@ -20,7 +20,7 @@ struct IpCoreRegisterBlock<IPCore, MemoryModel> {
}
private:
std::map<IPCore, MemoryModel> memoryblocks_{
std::map<IPCore, MemoryModel> memoryblocks_{
{IPCore::MH_RO_SM_AXI,
MemoryModel{static_cast<uint32_t>(IPCore::MH_RO_SM_AXI),
IPCORE_REGISTER_BLOCK_SIZE}},
@@ -0,0 +1,122 @@
#include "SPICommunication.h"
#include <fcntl.h>
#include <linux/spi/spidev.h>
#include <sys/ioctl.h>
#include <unistd.h>
namespace sls {
void HardwareSPICommunication::map_to_memory() {
// TODO device can change
int spi_filedescriptor = open("/dev/spidev2.0", O_RDWR); // TODO use O_SYNC?
LOG(logINFO) << fmt::format("SPI Read: opened spidev2.0 with fd={}",
spi_filedescriptor);
if (spi_filedescriptor < 0) {
LOG(logERROR) << "Could not open /dev/spidev2.0";
throw std::runtime_error("Could not open /dev/spidev2.0");
}
}
void HardwareSPICommunication::unmap_memory() {
if (spi_filedescriptor >= 0) {
close(spi_filedescriptor);
LOG(logINFO) << "SPI Read: closed spidev2.0";
}
}
std::vector<std::byte>
HardwareSPICommunication::spi_read(const size_t n_bytes, const uint8_t chip_id,
const uint8_t register_id) const {
// allocate dummy data to shift out the data (first byte is command byte)
std::vector<std::byte> dummy_data(n_bytes + 1);
// First byte of the message is 4 bits chip_id then 4 bits register_id
dummy_data[0] =
static_cast<std::byte>(((chip_id & 0xF) << 4) | (register_id & 0xF));
// allocate data buffer to read out data into
std::vector<std::byte> read_data_buffer(
n_bytes + 1, std::byte{0x00}); // TODO: is it neccessary to initialize?
spi_ioc_transfer send_cmd{};
send_cmd.len = n_bytes + 1; // +1 for the command byte
send_cmd.tx_buf = reinterpret_cast<std::uintptr_t>(dummy_data.data());
send_cmd.rx_buf = reinterpret_cast<std::uintptr_t>(read_data_buffer.data());
// 0 - Normal operation, 1 - CSN remains zero after operation
// We use cs_change = 1 to not close the SPI transaction and
// allow for shifting the read out data back in to restore the
// regitster
send_cmd.cs_change = 1;
// transfer here
if (ioctl(spi_filedescriptor, SPI_IOC_MESSAGE(1), &send_cmd) < 0) {
LOG(logERROR) << fmt::format("SPI write failed with {}:{}", errno,
strerror(errno));
throw std::runtime_error(
fmt::format("SPI write failed with {}:{}", errno, strerror(errno)));
}
// copy the read out data back to the dummy data buffer to shift it back in
send_cmd.tx_buf = send_cmd.rx_buf;
send_cmd.cs_change =
0; // end the SPI transaction after shifting back in the data
if (ioctl(spi_filedescriptor, SPI_IOC_MESSAGE(1), &send_cmd) < 0) {
LOG(logERROR) << fmt::format("SPI write failed with {}:{}", errno,
strerror(errno));
throw std::runtime_error(
fmt::format("SPI write failed with {}:{}", errno, strerror(errno)));
}
// copy data to output buffer
std::vector<std::byte> output_data(n_bytes);
std::memcpy(output_data.data(),
reinterpret_cast<std::byte *>(send_cmd.tx_buf) + 1, n_bytes);
return output_data;
}
void HardwareSPICommunication::spi_write(const uint8_t chip_id,
const uint8_t register_id,
const std::vector<std::byte> &data) {
const size_t n_bytes = data.size();
// First byte of the message is 4 bits chip_id then 4 bits register_id
std::vector<std::byte> write_data(n_bytes + 1); // +1 for the command byte
write_data[0] =
static_cast<std::byte>(((chip_id & 0xF) << 4) | (register_id & 0xF));
std::memcpy(write_data.data() + 1, data.data(), n_bytes);
std::vector<std::byte> read_back_buffer(n_bytes +
1); // TODO: do we need this?
spi_ioc_transfer send_cmd{};
send_cmd.len = n_bytes + 1; // +1 for the command byte
send_cmd.tx_buf = reinterpret_cast<std::uintptr_t>(write_data.data());
send_cmd.rx_buf = reinterpret_cast<std::uintptr_t>(read_back_buffer.data());
send_cmd.cs_change =
0; // end the SPI transaction after the write (we dont need to shift
// back in data here since we are not doing a read)
if (ioctl(spi_filedescriptor, SPI_IOC_MESSAGE(1), &send_cmd) < 0) {
LOG(logERROR) << fmt::format("SPI write failed with {}:{}", errno,
strerror(errno));
throw std::runtime_error(
fmt::format("SPI write failed with {}:{}", errno, strerror(errno)));
}
}
} // namespace sls
@@ -1,3 +1,4 @@
#pragma once
#include "fmt/format.h"
#include <cstdint>
#include <vector>
@@ -30,22 +31,28 @@ class HardwareMemoryModel {
/// @brief class to handle memory mapping and access for virtual IP cores (e.g.
/// use software implementation of memory)
class VirtualMemoryModel {
template <typename DataType> class VirtualMemoryModel {
public:
VirtualMemoryModel(const uint32_t IPcore_base_address,
const size_t size_memory_space_);
const size_t size_memory_space_)
: IPCore_base_address(IPcore_base_address),
size_memory_space(size_memory_space_) {}
~VirtualMemoryModel() = default;
void mapToMemory();
void mapToMemory() {
mapped_memory.resize(
size_memory_space /
sizeof(DataType)); // TODO: should it be zero initialized?
}
uint32_t *getMappedMemoryPtr();
DataType *getMappedMemoryPtr() { return mapped_memory.data(); }
const uint32_t *getMappedMemoryPtr() const;
const DataType *getMappedMemoryPtr() const { return mapped_memory.data(); }
private:
std::vector<uint32_t> mapped_memory{};
std::vector<DataType> mapped_memory{};
/// @brief offset of the IP core base address in the memory space, used for
/// mapping
@@ -51,23 +51,3 @@ void HardwareMemoryModel::unmapMemory() {
}
HardwareMemoryModel::~HardwareMemoryModel() { unmapMemory(); }
VirtualMemoryModel::VirtualMemoryModel(const uint32_t IPcore_base_address,
const size_t size_memory_space_)
: IPCore_base_address(IPcore_base_address),
size_memory_space(size_memory_space_) {}
void VirtualMemoryModel::mapToMemory() {
mapped_memory.resize(
size_memory_space /
sizeof(uint32_t)); // TODO: should it be zero initialized?
}
uint32_t *VirtualMemoryModel::getMappedMemoryPtr() {
return mapped_memory.data();
}
const uint32_t *VirtualMemoryModel::getMappedMemoryPtr() const {
return mapped_memory.data();
}