Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
133 changes: 133 additions & 0 deletions lib/mcal/cli/spi.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
/// @author Priyal Taneja
/// @date 2025-10-19

#pragma once
#include <cstdint>
#include <cstring>
#include <format>
#include <iostream>
#include <string>

#include "periph/spi.hpp"

namespace mcal::cli {

class SpiMaster : public macfe::periph::SpiMaster {
private:
std::string name_;

public:
SpiMaster(std::string name) : name_(name) {
std::cout << std::format("Created SPI Master {}", name_) << std::endl;
}

void Transmit(uint8_t* tx_data, size_t length) override {
std::cout << std::format("SPI {} Transmitting {} bytes: ", name_,
length);
for (size_t i = 0; i < length && i < 16; ++i) {
std::cout << std::format("{:02X} ", tx_data[i]);
}
if (length > 16) {
std::cout << "...";
}
std::cout << std::endl;
}

void Receive(uint8_t* rx_data, size_t length) override {
std::cout << std::format("SPI {} Receiving {} bytes", name_, length)
<< std::endl;
std::cout << " | Enter hex values (e.g., FF 00 A5) or press Enter for "
"default 0xFF: ";

// read line of input
std::string line;
std::getline(std::cin >> std::ws, line);

if (line.empty()) {
// default: fill with 0xFF
memset(rx_data, 0xFF, length);
} else {
// parse hex values from input
size_t idx = 0;
size_t pos = 0;
while (pos < line.length() && idx < length) {
if (std::isxdigit(line[pos])) {
unsigned int value;
std::sscanf(line.c_str() + pos, "%2x", &value);
rx_data[idx++] = static_cast<uint8_t>(value);
pos += 2;
} else {
pos++;
}
}
// fill remaining data with 0xFF
while (idx < length) {
rx_data[idx++] = 0xFF;
}
}

std::cout << " | Received: ";
for (size_t i = 0; i < length && i < 16; ++i) {
std::cout << std::format("{:02X} ", rx_data[i]);
}
if (length > 16) {
std::cout << "...";
}
std::cout << std::endl;
}

void TransmitReceive(uint8_t* tx_data, uint8_t* rx_data,
size_t length) override {
std::cout << std::format("SPI {} TransmitReceive {} bytes", name_,
length)
<< std::endl;
std::cout << " | TX: ";
for (size_t i = 0; i < length && i < 16; ++i) {
std::cout << std::format("{:02X} ", tx_data[i]);
}
if (length > 16) {
std::cout << "...";
}
std::cout << std::endl;

std::cout << " | Enter RX hex values or press Enter for default 0xFF: ";

// read line of input
std::string line;
std::getline(std::cin >> std::ws, line);

if (line.empty()) {
// default: fill with 0xFF
memset(rx_data, 0xFF, length);
} else {
// parse hex values from input
size_t idx = 0;
size_t pos = 0;
while (pos < line.length() && idx < length) {
if (std::isxdigit(line[pos])) {
unsigned int value;
std::sscanf(line.c_str() + pos, "%2x", &value);
rx_data[idx++] = static_cast<uint8_t>(value);
pos += 2;
} else {
pos++;
}
}
// fill remaining data with 0xFF
while (idx < length) {
rx_data[idx++] = 0xFF;
}
}

std::cout << " | RX: ";
for (size_t i = 0; i < length && i < 16; ++i) {
std::cout << std::format("{:02X} ", rx_data[i]);
}
if (length > 16) {
std::cout << "...";
}
std::cout << std::endl;
}
};

} // namespace mcal::cli
133 changes: 133 additions & 0 deletions lib/mcal/linux/spi.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
/// @author Priyal Taneja
/// @date 2025-11-06

#pragma once

#include <fcntl.h>
#include <linux/spi/spidev.h>
#include <sys/ioctl.h>
#include <unistd.h>

#include <cstdint>
#include <cstring>
#include <string>

#include "periph/spi.hpp"

namespace mcal::lnx {

class SpiMaster : public macfe::periph::SpiMaster {
private:
int fd_;
std::string device_path_;
uint32_t speed_hz_;
uint8_t mode_;
uint8_t bits_per_word_;
bool initialized_;

public:
/// constructor
/// @param device_path path to spi device (e.g., "/dev/spidev0.0")
/// @param speed_hz spi clock speed in Hz (default: 1 MHz)
/// @param mode spi mode 0-3 (default: SPI_MODE_0)
/// @param bits_per_word bits per word (default: 8)
SpiMaster(std::string device_path, uint32_t speed_hz = 1000000,
uint8_t mode = SPI_MODE_0, uint8_t bits_per_word = 8)
: fd_(-1),
device_path_(device_path),
speed_hz_(speed_hz),
mode_(mode),
bits_per_word_(bits_per_word),
initialized_(false) {}

~SpiMaster() {
if (fd_ >= 0) {
close(fd_);
}
}

/// initialize the spi device
/// opens the device file and configures spi parameters
void Setup() {
if (initialized_) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

see this comment https://github.com/macformula/racecar/pull/539/files#r2528588442, don't need the initialized field & checks

return;
}

// open spi device
fd_ = open(device_path_.c_str(), O_RDWR);
if (fd_ < 0) {
return;
}

// set spi mode
if (ioctl(fd_, SPI_IOC_WR_MODE, &mode_) < 0) {
close(fd_);
fd_ = -1;
return;
}

// set bits per word
if (ioctl(fd_, SPI_IOC_WR_BITS_PER_WORD, &bits_per_word_) < 0) {
close(fd_);
fd_ = -1;
return;
}

// set max speed
if (ioctl(fd_, SPI_IOC_WR_MAX_SPEED_HZ, &speed_hz_) < 0) {
close(fd_);
fd_ = -1;
return;
}

initialized_ = true;
}

void Transmit(uint8_t* tx_data, size_t length) override {
if (!initialized_ || fd_ < 0) {
return;
}

struct spi_ioc_transfer transfer = {0};
transfer.tx_buf = reinterpret_cast<__u64>(tx_data);
transfer.rx_buf = 0;
transfer.len = length;
transfer.speed_hz = speed_hz_;
transfer.bits_per_word = bits_per_word_;

ioctl(fd_, SPI_IOC_MESSAGE(1), &transfer);
}

void Receive(uint8_t* rx_data, size_t length) override {
if (!initialized_ || fd_ < 0) {
return;
}

struct spi_ioc_transfer transfer = {0};
transfer.tx_buf = 0;
transfer.rx_buf = reinterpret_cast<__u64>(rx_data);
transfer.len = length;
transfer.speed_hz = speed_hz_;
transfer.bits_per_word = bits_per_word_;

ioctl(fd_, SPI_IOC_MESSAGE(1), &transfer);
}

void TransmitReceive(uint8_t* tx_data, uint8_t* rx_data,
size_t length) override {
if (!initialized_ || fd_ < 0) {
return;
}

struct spi_ioc_transfer transfer = {0};
transfer.tx_buf = reinterpret_cast<__u64>(tx_data);
transfer.rx_buf = reinterpret_cast<__u64>(rx_data);
transfer.len = length;
transfer.speed_hz = speed_hz_;
transfer.bits_per_word = bits_per_word_;

ioctl(fd_, SPI_IOC_MESSAGE(1), &transfer);
}
};

} // namespace mcal::lnx
117 changes: 117 additions & 0 deletions lib/mcal/stm32f/spi.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
/// @author Priyal Taneja
/// @date 2025-10-19

#pragma once

#include <cstdint>

#include "periph/spi.hpp"

#ifdef STM32F7
#include "stm32f7xx_hal.h"
#elif defined(STM32F4)
#include "stm32f4xx_hal.h"
#endif

#ifdef HAL_SPI_MODULE_ENABLED

namespace mcal::stm32f {

class SpiMaster : public macfe::periph::SpiMaster {
private:
SPI_HandleTypeDef* hspi_;
GPIO_TypeDef* cs_port_;
uint16_t cs_pin_;
uint32_t timeout_ms_;
bool initialized_;

void ChipSelectLow() {
HAL_GPIO_WritePin(cs_port_, cs_pin_, GPIO_PIN_RESET);
}

void ChipSelectHigh() {
HAL_GPIO_WritePin(cs_port_, cs_pin_, GPIO_PIN_SET);
}

public:
/// constructor
/// @param hspi pointer to STM32 HAL SPI handle
/// @param cs_port gpio port for chip select pin (e.g., GPIOA)
/// @param cs_pin gpio pin number for chip select (e.g., GPIO_PIN_4)
/// @param timeout_ms timeout for SPI operations in milliseconds (default:
/// 100ms)
SpiMaster(SPI_HandleTypeDef* hspi, GPIO_TypeDef* cs_port, uint16_t cs_pin,
uint32_t timeout_ms = 100)
: hspi_(hspi),
cs_port_(cs_port),
cs_pin_(cs_pin),
timeout_ms_(timeout_ms),
initialized_(false) {}

/// initialize the SPI peripheral
/// must be called before using any transmission methods
/// typically called from bindings::Initialize()
void Init() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like the Init() function! it's a good idea since we want to guarantee that the CS pin is high on startup

I've seen the if(initialized_) and if(!initialized_) pattern used before, but I find it usually only clutters the code and doesn't provide any value. In Init() there is no harm in initializing twice (the outcome is the same), and in Transmit() / Receive() / TransmitReceive(), the transmission won't happen until you separately edit the code to call Init()

We don't need to protect the developer from forgetting to call Init(), so can you remove the initialized_ field and if(initialized_ checks? Similarly, we don't need to check that hspi != null

if (initialized_) {
return;
}

// initialize chip select pin as output, set high (inactive)
GPIO_InitTypeDef GPIO_InitStruct = {0};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

glad you learned stm HAL pin initialization, but you don't need to configure the chip select pin manually. we will configure in in CubeMX so you can assume the Port/Pin is is initialized already

GPIO_InitStruct.Pin = cs_pin_;
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
GPIO_InitStruct.Pull = GPIO_NOPULL;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
HAL_GPIO_Init(cs_port_, &GPIO_InitStruct);

// set CS high (inactive) by default
ChipSelectHigh();

initialized_ = true;
}

void Transmit(uint8_t* tx_data, size_t length) override {
if (!initialized_ || hspi_ == nullptr) {
return;
}

ChipSelectLow();
HAL_SPI_Transmit(hspi_, tx_data, length, timeout_ms_);
ChipSelectHigh();
}

void Receive(uint8_t* rx_data, size_t length) override {
if (!initialized_ || hspi_ == nullptr) {
return;
}

ChipSelectLow();
HAL_SPI_Receive(hspi_, rx_data, length, timeout_ms_);
ChipSelectHigh();
}

void TransmitReceive(uint8_t* tx_data, uint8_t* rx_data,
size_t length) override {
if (!initialized_ || hspi_ == nullptr) {
return;
}

ChipSelectLow();
HAL_SPI_TransmitReceive(hspi_, tx_data, rx_data, length, timeout_ms_);
ChipSelectHigh();
}

/// check if the spi peripheral has been initialized
bool IsInitialized() const {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

don't need this, see comment above

return initialized_;
}

/// get the underlying hal spi handle
SPI_HandleTypeDef* GetHandle() const {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we need this. When would it be called?

return hspi_;
}
};

} // namespace mcal::stm32f

#endif // HAL_SPI_MODULE_ENABLED