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.
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.
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.
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
RX0IFis set at one poll,buf1_dirtystays clear; if RXB1 then fills by rollover and RXB0 refills before the next poll, both flags are set withbuf1_dirtyfalse and the newer RXB0 frame is read first. One history bit cannot distinguish that case. - Not thread-safe. A plain
boolplus 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 wholereadMsgBuf()call. - Overruns are invisible.
EFLG.RX0OVR/RX1OVRare sticky and nothing clears them, so the drops this exists to prevent cannot be observed — andcheckError()latchesCAN_CTRLERRORpermanently after the first one.
sendMsg()does raw arithmetic onabsolute_time_t. Correct today, but it compiles only whilePICO_OPAQUE_ABSOLUTE_TIME_Tis 0.CMakeLists.txtsays${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 assignscfg1for 16 MHz withCAN_50KBPS, so that combination programs CNF1 from an uninitialised value.mcp2515_init()returnsMCP2515_OKwhenmcp2515_configRate()fails, so an unsupported clock or baud rate reports success.mcp_can_dfs.hexports around forty unprefixed macros —MODE_CONFIG,MODE_MASK,ABORT_TX,SAMPLE_1X,SJW1–SJW4,TIMEOUTVALUE,DEBUG_MODEamong them — which will rewrite consumer identifiers of the same name.MCP2515_SELECT()andMCP2515_UNSELECT()are public macros that reference a bareMCPCSmember, so they only compile insideMCP_CANmember functions.
LGPL 2.1 or later. See license.txt.