Skip to content
 
 

Repository files navigation

MCP_CAN — MCP2515 driver for the Raspberry Pi Pico SDK

Drives an MCP2515 or MCP25625 CAN controller over hardware/spi.h. Derived from the Arduino MCP_CAN library by Loovee and Cory J. Fowler.

The register map, bit timing tables and public API are the ones from the original library, so existing MCP_CAN code ports over with little more than a constructor change.

What the port changed

Bus access. SPI.transfer() byte loops became spi_write_blocking() / spi_read_blocking() over an spi_inst_t*. Arduino's beginTransaction() / endTransaction() pair is replaced by mcpSpiSelect(), invoked through the MCP2515_SELECT() macro: it restores SPI mode 0, 8-bit, MSB-first and this device's clock divider before asserting /CS, so a shared bus that another driver reconfigured is handled. The divider is derived once and cached — spi_set_baudrate() searches for the prescaler with a long divide loop, which is too expensive to repeat across the six transactions in a single readMsg().

SPI clock is MCP_SPI_BAUDRATE, default 500 kHz. The MCP2515 is rated for 10 MHz; raise it with -DMCP_SPI_BAUDRATE=... once you trust the wiring.

Chip select. digitalWrite() became gpio_put(), and the constructors call gpio_init() on the /CS pin themselves. That matters on RP2040: without gpio_init() the pad's function select stays at its reset value and SIO drives nothing, so gpio_set_dir() and gpio_put() write registers with no effect on the pin.

Timing. millis() / micros() became get_absolute_time(), delay() became sleep_ms(). Mode changes give up after 200 ms and return MCP2515_FAIL rather than retrying forever.

Debug output. Serial.print() became printf(), gated on DEBUG_MODE. It defaults to 1, so begin(), init_Mask() and init_Filt() are chatty; set it to 0 to silence them.

Types. byte became uint8_t and the INT32U / INT8U macros became uint32_t / uint8_t throughout.

Build. CMakeLists.txt produces an MCP_CAN_lib static library linked against pico_stdlib and hardware_spi.

Using it

The library owns /CS and the per-transaction SPI format. It does not own the SPI peripheral or its pins, which are board-specific — set those up first:

spi_init(spi1, 500 * 1000);
gpio_set_function(14, GPIO_FUNC_SPI);   // SCK
gpio_set_function(15, GPIO_FUNC_SPI);   // MOSI / TX
gpio_set_function(8,  GPIO_FUNC_SPI);   // MISO / RX

MCP_CAN mcp_can(spi1, 19);              // spi instance, /CS pin
mcp_can.begin(MCP_ANY, CAN_500KBPS, MCP_16MHZ);
mcp_can.setMode(MCP_NORMAL);

The single-argument MCP_CAN(uint8_t cs) constructor uses spi_default, standing in for Arduino's implicit &SPI.

begin() returning CAN_OK leaves the controller in loopback, which stabilises filter setup on a live bus. Call setMode(MCP_NORMAL) afterwards or nothing reaches the wire.

Supported crystals are 8, 16 and 20 MHz, selected by the third argument to begin().

readMsgBuf(*id, *dlc, *data) folds the ID type and remote-request bit into the ID: id & 0x80000000 means extended, id & 0x40000000 means remote request. readMsgBuf(*id, *ext, *dlc, *data) returns the ID unaltered and reports extended via ext, but does not report remote requests.

sendMsgBuf(id, dlc, data) reads those same two flag bits off the ID to send extended frames or remote requests. sendMsgBuf(id, ext, dlc, data) takes ext explicitly.

setMode() selects sleep, loopback, listen-only or normal. enOneShotTX() / disOneShotTX() control one-shot transmission, disabled by default. setSleepWakeup(1) enables wake-on-bus-activity while sleeping, also disabled by default.

Receive-buffer rotation

readMsg() alternates between RXB0 and RXB1 rather than always draining RXB0 first.

The MCP2515 fills RXB0 first and only rolls over into RXB1. Draining RXB0 by preference means that under sustained load it is refilled before the next poll, RX0IF is set again, and RXB1's frame is never serviced — it sits there getting steadily staler while the bus overruns. The buf1_dirty flag records "RXB1 already had a frame when I last drained RXB0" and forces the next read to take RXB1, which is also the correct order: the frame in RXB0 is older than the one that rolled into RXB1.

Known limits:

  • Not a total order. If only RX0IF is set at one poll, buf1_dirty stays clear; if RXB1 then fills by rollover and RXB0 refills before the next poll, both flags are set with buf1_dirty false and the newer RXB0 frame is read first. One history bit cannot distinguish that case.
  • Not thread-safe. A plain bool plus an unguarded multi-transaction SPI sequence. Fine for a single polling task; a second task or an ISR on the same instance needs a mutex around the whole readMsgBuf() call.
  • Overruns are invisible. EFLG.RX0OVR / RX1OVR are sticky and nothing clears them, so the drops this exists to prevent cannot be observed — and checkError() latches CAN_CTRLERROR permanently after the first one.

Known defects

  • sendMsg() does raw arithmetic on absolute_time_t. Correct today, but it compiles only while PICO_OPAQUE_ABSOLUTE_TIME_T is 0.
  • CMakeLists.txt says ${ProjectName} where it means ${PROJECT_NAME}, so the include directory lands on whatever target a parent scope happens to name rather than on this library. Configuring this directory on its own fails outright. file(GLOB HEADERS include/*.h) matches nothing.
  • mcp2515_configRate() never assigns cfg1 for 16 MHz with CAN_50KBPS, so that combination programs CNF1 from an uninitialised value.
  • mcp2515_init() returns MCP2515_OK when mcp2515_configRate() fails, so an unsupported clock or baud rate reports success.
  • mcp_can_dfs.h exports around forty unprefixed macros — MODE_CONFIG, MODE_MASK, ABORT_TX, SAMPLE_1X, SJW1SJW4, TIMEOUTVALUE, DEBUG_MODE among them — which will rewrite consumer identifiers of the same name.
  • MCP2515_SELECT() and MCP2515_UNSELECT() are public macros that reference a bare MCPCS member, so they only compile inside MCP_CAN member functions.

Licence

LGPL 2.1 or later. See license.txt.

About

MCP_CAN Library

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages