MCP Servers

A collection of Model Context Protocol servers, templates, tools and more.

M
Micropython Mcp2515

Standalone MicroPython driver for the MCP2515 CAN controller over SPI

Created 6/11/2026
Updated about 2 hours ago
Repository documentation and setup instructions

micropython-mcp2515

A clean, well-tested MicroPython driver for the MCP2515 CAN controller over SPI.

Designed for ESP32 (including ESP32-S2) but should work on any MicroPython port with machine.SPI and machine.Pin.

Features

  • Full send + receive support
  • Extended (29-bit) ID support for both transmit and receive (TX EID8/EID0 writes included)
  • RTR (remote frame) support via CANMessage(rtr=True) -- RTR bit set in DLC on send, extracted on recv
  • Multiple bitrates (including 250 kbps and 500 kbps common in vehicles)
  • 8 MHz, 16 MHz, 20 MHz, and 4 MHz crystal support (4 MHz common on cheap modules)
  • 1 Mbps support on 8/16/20 MHz where timing allows
  • Expanded table + compute_cnf helper (see Bitrates section)
  • Proper RX buffer configuration for reliable reception (RXM + BUKT)
  • Status / error reporting
  • Optional interrupt pin support
  • Debug helpers for diagnosing "why am I not seeing frames?"
  • Automatic bus-off recovery (check_and_recover) that restores the original mode from begin() + re-verifies (not hardcoded to NORMAL)
  • Hardware acceptance filter/mask API: set_acceptance_mask(idx, mask, extended), set_filter(idx, can_id, mask_idx, extended), clear_filters() (default remains promiscuous "receive all" like before)
  • Small and dependency-free
  • Burst/sequential SPI helpers (_write_registers / _read_registers) + refactored hot paths in send()/recv() for lower latency and fewer MicroPython allocations (see Performance)
  • Fast status commands: READ STATUS (0xA0) and RX STATUS (0xB0) via read_status() / rx_status() (return int + STAT/RXSTAT bitmasks from constants); integrated into recv()/wait_for_rx()/send()/get_status/print_status for cheaper polling (single-byte cmd + read vs full reg accesses) -- see Performance / Status note and examples/basic.py

Recent Improvements (High-Priority Items)

The core high-priority items implemented after initial release (fully integrated, documented, and demonstrated together):

  • IRQ-driven receive (use_irq + wait_for_rx) -- see Optional Interrupt Pin section.
  • Hardware filters & masks via set_acceptance_mask / set_filter / clear_filters -- see Acceptance Filters section.
  • Recovery that preserves begin() mode (check_and_recover, non-NORMAL support) -- see Bus-Off Recovery.
  • Expanded bitrates table (4/8/16/20 MHz, more rates incl. 20k/1M edge cases) + compute_cnf() helper in constants -- see Bitrates.

These (and context/close, strerror/last_error, richer status, burst perf, exception hierarchy) are 100% additive. All original behavior, ERROR codes, APIs, and the user's motivation (below) preserved. See examples/basic.py (top basic + new combined advanced section at bottom) for usage.

Performance

Burst I/O optimizations: the driver now performs contiguous register reads/writes (e.g. full RX frame header+data or TX payload) using a single SPI CS assertion + command + address + N bytes, instead of per-register transactions. This reduces bus overhead and heap churn on resource-constrained devices like ESP32. All public APIs, debug hooks, IRQ, filters, recovery, RTR/ext, etc. preserved exactly.

Optional pre-allocated receive buffer: pass rx_buffer=bytearray(8) (or larger) to MCP2515(...) constructor. When a frame arrives in recv(), if the buffer is large enough its payload is copied into the buffer and msg.data uses a memoryview over it (avoids fresh per-frame bytes allocation for CAN data, further reducing heap pressure on hot paths). Falls back to prior allocation behavior if omitted or too small. 100% non-breaking. Example:

rx_buf = bytearray(8)
can = MCP2515(spi, cs, rx_buffer=rx_buf)
# ... begin ...
err, msg = can.recv()
# if ok: msg.data is memoryview into rx_buf (len tells used length); reuse rx_buf for next frames

See mcp2515/__init__.py (recv + init doc) and examples/basic.py (commented demo).

Fast status polling (new): read_status() issues READ STATUS (0xA0) returning a byte (TXB n pending via STAT.TXBnREQ, RX pending via STAT.RX0IF etc); rx_status() for RX STATUS (0xB0) (RX buffer/filter/RTR/ext via RXSTAT.*). These are single-byte-cmd + 1-byte-read (no addr) -- faster for polling than CANINTF/TXBCTRL reads. Integrated into recv/wait_for_rx (RX check), send (TX free), get_status/print_status (new 'read_status'/'rx_status' dict keys, additive). All prior behavior/debug/IRQ/filters/recovery/ext/RTR/API preserved 100%. Usage:

from mcp2515.constants import STAT, RXSTAT
s = can.read_status()
if s & STAT.RX0IF: ...  # or use in wait_for_rx etc
rs = can.rx_status()
if rs & RXSTAT.RTR: ...
print(can.get_status()["read_status"])  # or can.print_status()

See examples/basic.py for live demo and mcp2515/__init__.py + constants.py (CMD/STAT/RXSTAT).

Installation

Recommended (MicroPython mip)

mip install github:cozadizzle/micropython-mcp2515

Manual

Copy the mcp2515/ folder into your project so you can do:

from mcp2515 import MCP2515

Wiring (ESP32-S2 example)

| MCP2515 | ESP32 GPIO | Notes | |---------|------------|------------------------| | SCK | 18 | SPI clock | | MOSI | 11 | SPI data out | | MISO | 9 | SPI data in | | CS | 5 | Chip select (active low) | | CANH | - | To OBD pin 6 | | CANL | - | To OBD pin 14 |

SPI bus: SPI(1)

Important:

  • Remove the 120Ω termination jumper on the MCP2515 module when using it on a real vehicle (the car provides termination).
  • Use a proper CAN transceiver on the MCP2515 board (SN65HVD230 or equivalent recommended).

Basic Usage

from machine import Pin, SPI
from mcp2515 import MCP2515, CANMessage

spi = SPI(1, baudrate=10_000_000, polarity=0, phase=0,
          sck=Pin(18), mosi=Pin(11), miso=Pin(9))
cs = Pin(5, Pin.OUT)

can = MCP2515(spi, cs)

# 500 kbps with 8 MHz crystal (common on vehicles)
can.begin(bitrate=500000, clock=8000000)

# Send a frame
msg = CANMessage(can_id=0x123, data=b'\x01\x02\x03')
can.send(msg)

# Send remote frame (RTR): rtr=True, usually with data=b'' (DLC=0); extended IDs also supported
# rtr_msg = CANMessage(can_id=0x7DF, rtr=True)
# can.send(rtr_msg)
# ext_msg = CANMessage(can_id=0x18DAF110, data=b'\x01', extended=True)
# can.send(ext_msg)

# Receive (non-blocking) -- .rtr and .extended are populated on received msgs
err, msg = can.recv()
if err == 0:  # ERROR.OK
    print(msg.can_id, msg.data)

Context Manager and Cleanup

The MCP2515 now supports the context manager protocol (__enter__/__exit__) and a public close() method for resource niceties:

with MCP2515(spi, cs) as can:
    can.begin(bitrate=500000, clock=8000000)
    # ... send/recv, filters, IRQ use_irq, etc. ...
# On exit (normal or exception): close() is called, resetting chip to safe SLEEP state
# and clearing begin() config state. Explicit use also supported: can.close()

# See examples/basic.py for more.

close() (and thus __exit__) performs a reset + sets MODE.SLEEP (low-power safe state), clears recovery state. IRQ pin registration and all other behavior is left for object lifetime / re-use. All prior features (use_irq, filters, recovery, ext-ID, RTR, debug, compute_cnf, etc.) are 100% preserved.

mip packaging

mip install (see above) is supported via a mip.json in the repo root (points to the mcp2515/ package for proper installation as importable module). Version is "0.3.0" (matches __version__ and mcp2515.constants.VERSION).

Combined Example / Advanced Usage

To see IRQ (use_irq=True + wait_for_rx), filters (set_acceptance_mask + set_filter), recovery (begin with non-NORMAL mode e.g. LOOPBACK + check_and_recover), and compute_cnf/expanded bitrates used together in one place, refer to the comprehensive "COMBINED ADVANCED EXAMPLE" section at the bottom of examples/basic.py.

(The very top of examples/basic.py remains the original simple send/recv for basic cases + the combined demo at bottom. Dedicated host mock tests live in examples/test_mock.py; run with plain python examples/test_mock.py (uses importlib+sys.modules mocks for machine etc).)

Optional Interrupt Pin

The driver supports an optional int_pin (for the MCP2515's active-low ~INT output).

Basic wiring (add to your SPI+CS):

int_pin = Pin(21, Pin.IN, Pin.PULL_UP)
can = MCP2515(spi, cs, int_pin=int_pin)

Enabling real IRQ-driven receive (High Priority Item #1)

Pass use_irq=True (new option) and provide int_pin. This:

  • Calls int_pin.irq(trigger=Pin.IRQ_FALLING, handler=...) (falling because ~INT is active-low).
  • On begin(), enables RX0IE|RX1IE in CANINTE and clears stale flags.
  • Sets up a simple _rx_pending flag (set from IRQ context).
  • Re-enables on check_and_recover() too.
int_pin = Pin(21, Pin.IN, Pin.PULL_UP)
can = MCP2515(spi, cs, int_pin=int_pin, use_irq=True)
can.begin(bitrate=500000, clock=8000000)

# Block until IRQ (or chip status) indicates RX frame ready, with optional timeout
if can.wait_for_rx(timeout_ms=2000):
    err, msg = can.recv()
    if err == 0:
        print("Got via IRQ:", msg.can_id, msg.data, "rtr=", msg.rtr, "ext=", msg.extended)
else:
    print("No frame (timeout)")

You can also call recv() directly anytime (it checks the _rx_pending flag first if IRQ mode, then always runs the original CANINTF poll logic to extract/clear the frame).

See examples/basic.py (combined section) for an example mixing use_irq + wait_for_rx with the other recent features (filters, recovery, compute_cnf).

Polling vs IRQ trade-offs

  • Default (polled): MCP2515(spi, cs) or with int_pin but without use_irq=True. recv() is a fast non-blocking poll on CANINTF.RX0IF/RX1IF. Extremely reliable, all debug hooks (enable_debug_recv(True), print_status(), register dumps in recv) work exactly as before, no IRQ context restrictions. Recommended for most car diag / reverse-engineering use.
  • IRQ mode: Saves CPU by not spinning recv() in a tight loop when idle. wait_for_rx() blocks (with time.ticks + 1ms sleeps) until the flag or CANINTF shows data. Still requires your main code to call wait/recv (IRQ handler only sets a flag; MicroPython IRQ handlers must stay tiny). The actual message read + ext-id/RTR handling + clearing + debug always goes through the proven polled path inside recv().
  • If a bus-off recovery happens, IRQ enables are automatically restored.
  • Back-compat: old code passing int_pin positionally or without use_irq continues to get pure polled behavior; the pin is simply initialized but otherwise inert.

All existing APIs, error codes, CANMessage fields, begin args, debug, send, status, RTR/ext fixes, etc. are 100% preserved. wait_for_rx and the use_irq kwarg are additive only.

Bitrates

Common vehicle rates:

  • 500000 (most cars, including many Honda powertrains)
  • 250000 (some older vehicles, e.g. certain Volvos)

Pass the correct clock for your MCP2515 crystal (usually 8000000).

Supported crystals and bitrates (expanded)

  • 8 MHz: 1M, 500k, 250k, 125k, 100k, 50k, 20k
  • 16 MHz: 1M, 500k, 250k, 125k, 100k, 50k, 20k
  • 20 MHz: 1M, 500k, 250k, 125k, 100k, 50k, 20k
  • 4 MHz (cheap modules): 500k, 250k, 125k, 100k, 50k, 20k

All via the BITRATE table in mcp2515/constants.py (lookup by (clock, bitrate) in begin()).

compute_cnf helper

For custom rates or validation:

from mcp2515.constants import compute_cnf
cnf1, cnf2, cnf3 = compute_cnf(4000000, 250000)           # 250k @ 4MHz xtal
# or with explicit segments (must yield exact BRP, total TQ 4-25):
cnf = compute_cnf(16000000, 1000000, sjw=1, prop_seg=1, ps1=3, ps2=3, sam=0)
  • Validates inputs; raises ValueError for impossible combos (use table entries for legacy non-exact cases).
  • Uses integer-only math; sets BTLMODE=1 and matches table style for CNF3 bit7.
  • Does not change begin() behavior (still table-driven for efficiency + no-calc on constrained device); helper is for advanced use or future table growth.
  • See compute_cnf source/docstring for details.

See examples/basic.py (combined advanced example) for usage of compute_cnf together with IRQ, filters, and recovery (begin non-NORMAL).

Debugging Receive Issues

The driver includes helpers that were extremely useful during vehicle development:

can.print_status()
# or
from mcp2515.constants import ERROR
err, msg = can.recv()

Advanced Debug Helpers (ported)

Optional helpers now ported/adapted into the library (top-level debug_can_receive(can, ...) style + debug_dump_registers(can), plus equivalent methods on the instance). These rapidly sample/print INTF/EFLG/RXB state or do extended raw dumps (TEC/REC + full CNF1/2/3 + more) for "why no frames?" diagnosis. They integrate with enable_debug_recv(True) (the per-poll hook inside recv remains unchanged).

from mcp2515 import MCP2515, debug_can_receive, debug_dump_registers
# ...
can.enable_debug_recv(True)
debug_can_receive(can, iterations=50, delay_ms=1)  # top-level (or can.debug_can_receive)
debug_dump_registers(can)
# can.debug_dump_registers()
can.enable_debug_recv(False)

See examples/basic.py (comments + combined advanced demo section that calls them) and the docstrings in mcp2515/__init__.py. All are optional, well-documented, and preserve 100% of prior behavior.

Error Handling

Integer ERROR codes from begin/send/recv/set_*/clear_filters are unchanged (0=OK etc; see constants.py) for speed/compat. New richer support:

  • can.last_error is set automatically on every error-returning call (init OK).
  • can.strerror(code=None) (or error_string()) returns human text for a code or last_error.
  • Lightweight exceptions in mcp2515 (and constants): MCP2515Error (base), BusOffError, WrongBitrateError, InitError, AllTxBusyError, NoMessageError. Use MCP2515Error.from_code(err) factory.
  • get_status() / print_status() now richer: includes "tec", "rec" (counters at REG 0x1C/0x1D), plus tx_warning/rx_warning/error_warning, errif/merrf (plus prior keys like bus_off unchanged).
  • Usage:
    from mcp2515 import MCP2515, MCP2515Error
    from mcp2515.constants import ERROR
    err = can.begin(...)
    if err != ERROR.OK:
        print(can.strerror(err))  # or can.last_error
        # raise MCP2515Error.from_code(err)
    st = can.get_status(); print(st["tec"], st["rec"])
    can.print_status()  # shows TEC/REC + new flags with colors
    

Bus-Off Recovery

check_and_recover() (called automatically from send() and recv()) detects bus-off via get_status()["bus_off"] (EFLG.TXBO).

Improvements:

  • Remembers the mode passed to begin() (e.g. LISTENONLY=0x60, LOOPBACK=0x40, NORMAL=0x00) as self._original_mode.
  • Recovery sequence (same as before but improved): reset, CONFIG, re-apply CNFs from _last_cnf, re-apply RXM+BUKT buffers (prevents deafness), set the original mode (not MODE.NORMAL), 50ms sleep.
  • Re-verifies the mode after recovery using (CANSTAT & 0xE0) != target_mode (mirrors the check in begin()); warns on mismatch but proceeds.
  • Handles pre-begin() or missing state: prints diagnostic and skips (avoids acting on uninitialized chip).
  • set_mode() / helpers are for manual runtime changes; recovery targets the begin() mode.
  • Manual trigger: can.check_and_recover(); monitor with can.print_status() or get_status().

This preserves all prior behavior, recent fixes (ext ID, RTR, RX config), debug features, etc.

Acceptance Filters and Masks (High Priority Item #2)

By default, begin() (and recovery) put the MCP2515 into promiscuous "receive all" mode: RXM1|RXM0 set in both RXBnCTRL (bypasses filters per datasheet) + masks zeroed. This is unchanged for backward compat and is great for sniffing/diagnostics.

To enable hardware-level filtering (frames that don't match are dropped by the controller, never reach recv()):

from machine import Pin, SPI
from mcp2515 import MCP2515, CANMessage
# (also: from mcp2515.constants import ERROR)

can = MCP2515(spi, cs)
can.begin(bitrate=500000, clock=8000000)

# Accept *only* std 11-bit ID 0x123 (use full mask for exact match):
can.set_acceptance_mask(0, 0x7FF)          # mask for std IDs
can.set_filter(0, 0x123, mask_idx=0)       # filter 0 governed by mask 0

# Or for a family: looser mask e.g. 0x7F0 matches 0x120-0x12F range etc.
# can.set_acceptance_mask(0, 0x7F0)
# can.set_filter(0, 0x120, mask_idx=0)

# Extended (29-bit) example (note mask_idx=1 + filter idx>=2 for mask1):
can.set_acceptance_mask(0, 0x1FFFFFFF, extended=True)
can.set_filter(2, 0x18DAF110, mask_idx=1, extended=True)

# Multiple filters ok under one mask (e.g. accept 0x123 or 0x124):
# can.set_filter(1, 0x124, mask_idx=0)

# Return to default promiscuous at any time:
can.clear_filters()
# (or just call begin() again, which resets everything including to receive-all)

Notes:

  • Call set_* / clear_filters() after begin() (or after any re-begin).
  • set_acceptance_mask(idx=0|1, mask, extended=False)
  • set_filter(idx=0..5, can_id, mask_idx=0|1, extended=False)
  • mask_idx documents the mask association (0 for filters 0/1, 1 for 2-5); hardware enforces it.
  • The set methods automatically reconfigure RXBnCTRL to RXM=00 (use filters) while keeping BUKT.
  • On error (bad idx): returns ERROR.INVALID_ARG (0 otherwise). Safe to ignore return.
  • Extended IDs set the EXIDE bit (and mask bit) using the exact same packing as the send/recv ext fixes.
  • Default remains receive-all unless you call a set_ method. clear_filters() restores it explicitly.
  • Bus-off recovery forces receive-all (existing behavior); re-apply your filter config after if desired.
  • All other behavior (ext, RTR, debug, IRQ, recover, status, ...) 100% preserved.
  • See examples/basic.py and mcp2515/constants.py (FILTER, RX, REG for the RXF*/RXM* + EID addrs).

License

MIT

Background & Motivation

I was sick of other MCP2515 MicroPython repos being abandoned, full of inconsistencies, and requiring heavy troubleshooting and modification just to get basic send/receive working reliably on real vehicles. Many had broken RX logic, incorrect buffer config, or hard-coded assumptions that didn't match the datasheet or actual hardware behavior (especially the critical RXM + BUKT + proper INTF handling).

This library was extracted and hardened from real vehicle reverse-engineering work (see the parent esp32-car-tool project) where reliable CAN was non-negotiable.

Credits

Extracted from the esp32-car-tool project (a portable vehicle diagnostic / reverse engineering tool). Many thanks to the community for the original reverse-engineering that exposed the bugs in other drivers.

Quick Setup
Installation guide for this server

Install Package (if required)

uvx micropython-mcp2515

Cursor configuration (mcp.json)

{ "mcpServers": { "cozadizzle-micropython-mcp2515": { "command": "uvx", "args": [ "micropython-mcp2515" ] } } }