By now I have a solid setup w/ the MCP2518fd. It took me several months, but it's all working now.
Unfortunately I can't share my code, but I should be able to give you some pointers.
I have settled on using
https://github.com/Emandhal/MCP251XFD, which is pretty simple to integrate and handles all the nitty low-level details. You just need to provide a single function to communicate via SPI and then use it to write a "real" driver.
My integration function looks like roughly like that:
Code: Select all
static eERRORRESULT mcp251x_spi_transfer(void *pIntDev, uint8_t chipSelect, uint8_t *txData, uint8_t *rxData, size_t size) {
if (!txData) {
return ERR__SPI_PARAMETER_ERROR;
}
auto object = reinterpret_cast<ESP_MCP251XFD*>(pIntDev);
auto locker = CriticalSection(object->spiSemaphore);
auto result = object->spiDevice->transfer(txData, rxData, size);
if (result != ESP_OK) {
ESP_LOGE(TAG, "SPI Communication Error: %d", result);
return ERR__SPI_COMM_ERROR;
}
return ERR_NONE;
}
I'm locking all the transfers from this driver, since I may call the driver functions from multiple tasks. My SPI communication's transfer method looks like that:
Code: Select all
esp_err_t Device::transfer(uint8_t* txdata, uint8_t* rxdata, size_t length) {
spi_transaction_t t = {
.flags = 0,
.cmd = 0,
.addr = 0,
.length = length * 8, // write register number, write value
.rxlength = length * 8,
.user = nullptr,
.tx_buffer = txdata,
.rx_buffer = rxdata,
};
auto result = spi_device_polling_transmit(handle, &t);
return result;
}
So you see I'm not using cmd or addr or any translation, but just transparently feeding the commands from the low-level driver to SPI and vice versa.
The basic higher level idea for the driver is that I setup an IRQ connected to the MCP's INT line. This one notifies a high priority task (using task notifications) which then calls the respective lower level functions to read from / send to FIFOs, etc.
Here's the IRQ handler:
Code: Select all
//----------------------------------------------------------------
// ISR
//----------------------------------------------------------------
void IrqHandlerTask::irqHandlerISR(void* arg) {
auto irqHandler = static_cast<IrqHandlerTask*>(arg);
#ifdef ESP_MCP251XFD_ENABLE_IRQ_DEADLOCK_TIMER
// Create irq deadlock timer, if not existing
if (!irqHandler->irqDeadlockTimer) {
const esp_timer_create_args_t timerConfiguration = {
.callback = &irqHandlerDeadlockTimerCallback,
.arg = arg,
.dispatch_method = ESP_TIMER_TASK,
.name = "IRQ-Deadlock-Timeout",
.skip_unhandled_events = false,
};
esp_timer_create(&timerConfiguration, &irqHandler->irqDeadlockTimer);
}
#endif
#ifdef ESP_MCP251XFD_ENABLE_IRQ_DEADLOCK_TIMER
bool irqActive = !gpio_get_level(irqHandler->device->irqPin);
if (irqActive) {
//ESP_EARLY_LOGI("IRQ", "Launching deadlock timer...");
esp_timer_start_once(irqHandler->irqDeadlockTimer, 1000000);
#endif
// Wake up IRQ handler task
auto xHigherPriorityTaskWoken = irqHandler->notifyGiveFromISR();
portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
#ifdef ESP_MCP251XFD_ENABLE_IRQ_DEADLOCK_TIMER
} else {
esp_timer_stop(irqHandler->irqDeadlockTimer);
//ESP_EARLY_LOGI("IRQ", "Cancelling deadlock timer...");
}
#endif
}
On top of all that, I have a simple API for the consumers. Here's the header file to give you an idea:
Code: Select all
class ESP_MCP251XFD {
friend class IrqHandlerTask;
enum Alert: uint8_t {
None = 0,
BusError = 1 << 0,
ReceiveOverflow = 1 << 5,
};
public:
static std::unique_ptr<ESP_MCP251XFD> createDevice(std::unique_ptr<SPI::Device> device, gpio_num_t irqPin, uint8_t xtalMHz = 40, bool gpiosControlTransceiverStandby = false);
/// Start the device in normal mode with the requested `nominal` bitrate and, if mixed CAN/CAN-FD is requested, the `data` bitrate.
bool start(uint32_t nominal, uint32_t data = 0, uint32_t minimumInterframeInterval = 0);
/// Start the device in receive-only mode, with the requested `nominal` bitrate and, if mixed CAN/CAN-FD is requested, the `data` bitrate.
bool startReceiveOnly(uint32_t nominal, uint32_t data = 0);
/// Stop the device.
void stop();
/// Release resources for the MCP251XFD device.
virtual ~ESP_MCP251XFD();
// Delete copy constructor (we're encapsulating a hardware resource)
ESP_MCP251XFD(const ESP_MCP251XFD&) = delete;
// Delete copy assignment operator (dito)
ESP_MCP251XFD& operator=(const ESP_MCP251XFD&) = delete;
/// Auto-detect the bitrate.
uint32_t autodetectBitrate(std::vector<uint32_t>& bitrates);
/// Update the acceptance filter.
bool updateFilter(uint32_t mask, uint32_t pattern);
/// Update the bitrate.
bool updateBitrate(uint32_t nominal, uint32_t data = 0, uint32_t minimumInterfaceInterval = 0);
/// Transmit a single frame.
bool transmit(const CANMessage& message);
/// Transmit multiple frames.
bool transmit(std::vector<CANMessage>& messages);
/// Receive a single frame.
bool receive(CANMessage** message, TickType_t timeout = portMAX_DELAY);
/// Return the current set of alerts and reset it.
Alert currentAlerts() {
auto alertAccess = CriticalSection(alertMutex);
Alert currentAlerts = alerts;
alerts = Alert::None;
return currentAlerts;
}
public:
uint32_t bitrate; // in bits per second
uint32_t sysclk; // in Hz
uint32_t interframeInterval; // in microseconds
uint32_t timestamp();
public /* private */:
std::unique_ptr<SPI::Device> spiDevice = nullptr;
MCP251XFD* mcpDevice = nullptr;
SemaphoreHandle_t spiSemaphore = NULL;
bool spi_debug_enabled = false;
bool irqHandlingEnabled = false;
protected:
void startIRQHandling();
void handleIRQ(); // Called from IRQ Handler task
void handleIRQDeadlockTimeout(); // Called from FreeRTOS Timer task
void stopIRQHandling();
void sleep();
void updateAlerts(Alert alert) {
auto alertAccess = CriticalSection(alertMutex);
alerts = static_cast<Alert>(static_cast<uint8_t>(alerts) | static_cast<uint8_t>(alert));
}
// Transmit a message to the TX FIFO. @returns true, if there is no space.
bool transmitToFifoIfPossible(const CANMessage& message);
private:
ESP_MCP251XFD(std::unique_ptr<SPI::Device> device, gpio_num_t irqPin, uint8_t xtalMHz, bool gpiosControlTransceiverStandby);
private:
gpio_num_t irqPin;
uint8_t xtalMHz;
bool gpiosControlTransceiverStandby;
std::unique_ptr<IrqHandlerTask> irqHandler = nullptr;
std::unique_ptr<Queue<CANMessage*>> receiveQueue = nullptr;
Mutex outgoingMessagesMutex;
std::queue<CANMessage> outgoingMessages;
Mutex alertMutex;
Alert alerts = Alert::None;
};
The client usually has a mid-priority task which then can block in receive to wait for the next message from the bus.
I have some more C++-abstractions for tasks, SPI bus, SPI master, and stuff. But this is roughly it. The only
critical thing (which took me a while) is to figure out when/how to service the FIFOs, since the MCP IRQ handling
is a bit limited. But in contrast to other solutions I found on github, mine is a fully IRQ-based driver now with no polling whatsoever.
I hope this helps.