Skip to content

stm32: Add machine.CAN low-level driver#18572

Merged
dpgeorge merged 5 commits into
micropython:masterfrom
projectgus:feature/machine_can
Mar 19, 2026
Merged

stm32: Add machine.CAN low-level driver#18572
dpgeorge merged 5 commits into
micropython:masterfrom
projectgus:feature/machine_can

Conversation

@projectgus
Copy link
Copy Markdown
Contributor

@projectgus projectgus commented Dec 15, 2025

Summary

This work follows on from #13149 and closes #12337. The goal is to have a machine.CAN API that can be used on multiple ports, and an implementation that works on stm32 port. The existing pyb.CAN API is being kept alongside this.

The machine.CAN API has changed substantially from the draft submitted in #13149. See comment with more explanation about what happened.

There's also one change in the old pyb.CAN API, which is that recv() now always returns a list rather than either a list or a tuple depending on how it is called. This allows using a new shared helper function that's added in one of the commits, but this change could also be pulled out at a small code size cost.

Status

Documented functionality is implemented, and the supplied new tests all pass.

  • Decision about scoped Enums (see comment).
  • Implement CAN.recv() error flags and add test coverage.
  • Implement controller errors: CAN.get_state(), CAN.State enum, CAN.IRQ_STATE interrupt trigger.
  • Implement CAN.get_counters()
  • Implement CAN.get_timings()
  • Implement CAN.reset() and CAN.restart(), and/or simplify the API to remove one of these.
  • Implement CAN.init(mode=x) and add test coverage. (Note: Previously there was going to be a CAN.mode() function to change modes on the fly, but this isn't really supported by the controller so instead have to deinit() and then init() into a new mode. There is test coverage for doing this.
  • Verify pyb.CAN still works, using multi_pyb_can tests.
  • Look at possible code size reductions (note: didn't find a lot of low hanging fruit here, unfortunately.)

Future PRs

  • Use target_wiring in the multi_extmod/machine_can_* tests. Currently these test cases are all hard-wired for CAN instance 1.
  • Create can & aiocan modules in micropython-lib. The goal of machine.CAN is to only implement the bare bones needed to make a higher level "Pythonic" CAN library on top.
  • Implement CAN-FD support (the current PR works on STM32 CAN-FD controller peripherals, but only in Classic CAN mode).

Testing

Ran pyb.CAN and new machine.CAN tests between pairs of NUCLEO_H723ZG board (fdcan peripheral), NUCLEO_G474RE board (simpler fdcan peripheral), and a PYBDV11 (bxCAN peripheral).

Specific commands:

./run-tests.py -t a0 ports/stm32/pyb_can*.py extmod_hardware/machine_can*.py

and for each pairing of the three boards:

./run-multitests.py -p2 -t a0 -t a1 multi_extmod/machine_can_*.py multi_pyb_can/*.py

Also ran the all the same tests with assertions on for all three boards (setting DEBUG=1 plus COPT=-Os for the G4 board). Some timeouts needed tweaking for these tests to pass.

In order to run machine_can2.py and pyb_can2.py tests on STM32H7, the same patch described in #18946 was applied.

Trade-offs and Alternatives

  • The biggest trade-off here was deciding to abandon the more ambitious API design from docs: Add machine.CAN low-level API docs. #13149 as it was too hard to implement properly. 😆. This pushes some more of the complexity into the eventual micropython-lib libraries, but it should be easier to implement all of it in Python now the Python-C interface is simpler.

@projectgus projectgus added port-stm32 extmod Relates to extmod/ directory in source labels Dec 15, 2025
Comment thread docs/library/machine.CAN.rst Outdated
Comment thread extmod/machine_can_port.h
@codecov
Copy link
Copy Markdown

codecov Bot commented Dec 15, 2025

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 98.45%. Comparing base (d1c936d) to head (6cac2d2).
⚠️ Report is 5 commits behind head on master.

Additional details and impacted files
@@           Coverage Diff           @@
##           master   #18572   +/-   ##
=======================================
  Coverage   98.45%   98.45%           
=======================================
  Files         175      175           
  Lines       22635    22646   +11     
=======================================
+ Hits        22286    22297   +11     
  Misses        349      349           

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@github-actions
Copy link
Copy Markdown

github-actions Bot commented Dec 15, 2025

Code size report:

Reference:  stm32: Expose FDCAN2 on board NUCLEO_G474RE. [d1c936d]
Comparison: stm32: Add machine.CAN implementation. [merge of 6cac2d2]
  mpy-cross:    +0 +0.000% 
   bare-arm:    +0 +0.000% 
minimal x86:    +0 +0.000% 
   unix x64:    +0 +0.000% standard
      stm32: +4440 +1.126% PYBV10[incl +8(bss)]
      esp32:   -28 -0.002% ESP32_GENERIC[incl -32(data)]
     mimxrt:    +0 +0.000% TEENSY40
        rp2:    +0 +0.000% RPI_PICO_W
       samd:    +0 +0.000% ADAFRUIT_ITSYBITSY_M4_EXPRESS
  qemu rv32:    +0 +0.000% VIRT_RV32

@sveinungkb
Copy link
Copy Markdown

Thanks for keeping at this! I've been using the previous branch for a while.

Comment thread tests/multi_can/remote_req.py Outdated
Comment thread tests/multi_extmod/machine_can_03_rx_filters.py Outdated
@dpgeorge
Copy link
Copy Markdown
Member

Thanks for putting this PR up!

I tested with 2x PYBV10 connected through some CAN tranceivers and the new tests added here all pass.

Testing the existing multi_pyb_can tests, the multi_pyb_can/rx_callback.py test fails with TIMEOUT. The other test still passes.

I noticed there's not a simple multitest that just sends and recv's a few frames, without using IRQs. I managed to get something simple working with a poll on can.recv() and it worked. Maybe it's worth adding a simple test like that? At least it would test that the CAN link is up and working, and serves as the simplest, working example.

Comment thread tests/multi_extmod/machine_can_06_remote_req.py
Comment thread docs/library/machine.rst Outdated
Comment thread extmod/extmod.mk Outdated
Comment thread docs/library/machine.CAN.rst Outdated
Comment thread docs/library/machine.CAN.rst Outdated
@projectgus
Copy link
Copy Markdown
Contributor Author

Still quite a bit to do (next year now), but it's coming along!

Also have some tests written for error state transitions and restart, but they need a little bit of tidying up.

Comment thread docs/library/machine.CAN.rst
@projectgus projectgus force-pushed the feature/machine_can branch 2 times, most recently from 3e498df to c94131c Compare February 4, 2026 06:25
@projectgus projectgus force-pushed the feature/machine_can branch 5 times, most recently from 1cae2ab to efee6da Compare February 18, 2026 07:16
@projectgus projectgus force-pushed the feature/machine_can branch 2 times, most recently from 4732f7e to 86c57a0 Compare February 19, 2026 03:54
@projectgus projectgus marked this pull request as ready for review February 19, 2026 04:14
@projectgus
Copy link
Copy Markdown
Contributor Author

I think this is ready for review, I've updated the PR description with the current status. Still plenty of follow-up work (also listed in description).

The one thing I'm not 100% happy with is the code size hit of +4400 bytes on PYBV10. If I manually disable pyb.CAN and rebuild I get back 3864 bytes, so the new driver is not a lot bigger than the old driver (and has a more flexible API design) - but it's still a big hit.

@dpgeorge dpgeorge self-requested a review March 12, 2026 13:35
@dpgeorge dpgeorge dismissed their stale review March 12, 2026 13:36

Found multiple bugs that must be fixed.

@dpgeorge
Copy link
Copy Markdown
Member

After closer inspection, there are a few remaining bug which must be fixed before this PR can be merged.

Background: @andrewleech has been working on an LLM review bot which @mattytrentini tested against this PR. See the results here: mattytrentini#35 . I went through the LLM review very carefully and triaged all of its comments (see linked PR). I will repeat/rephrase the important ones here.

Comment thread extmod/machine_can.c Outdated
Comment thread extmod/machine_can.c Outdated
Comment thread extmod/machine_can.c Outdated
Comment thread extmod/machine_can.c

machine_can_obj_t *self = MP_STATE_PORT(machine_can_objs)[can_idx];
if (self == NULL) {
self = mp_obj_malloc(machine_can_obj_t, &machine_can_type);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Not needed now, but I think we need to eventually define an mp_obj_malloc0() and use it in places like this where all fields need to be initialised to zero (in your previous revision of this part of the code it did zero everything out explicitly by assigning to the struct).

Comment thread extmod/machine_can.c Outdated
Comment thread ports/stm32/pyb_can.c Outdated
Comment thread extmod/extmod.mk Outdated
Comment thread extmod/machine_can_port.h Outdated
Comment thread ports/stm32/can.c
Comment thread ports/stm32/fdcan.c Outdated
@dpgeorge
Copy link
Copy Markdown
Member

Just to clarify my review comments above: a lot of the points are minor but I added them because I was anyway doing a full re-review and they caught my attention. The show stoppers for this PR are: null dereference in IRQ handler, messed up mode constants for FDCAN due to <<4, and confusion around the meaning ofauto_restart.

Also, a fourth show stopper is that the existing tests/ports/stm32/can.py test now fails. I should have run that myself earlier but forgot about it, and only remembered when I wanted to test LOOPBACK mode (which is what that test uses). Some points about that test:

  • it fails on bxCAN primarily because recv() return type changed from a tuple to list; that's pretty easy to fix, just change the corresponding .exp file
  • it seems that send() got optimised a bit and timeout=3 is no longer long enough, so the test needs can.send("fail", id_fail, timeout=4, extframe=True) instead (might be worth making it timeout=5 for both cases that use timeout=3 just to be on the safe side)
  • this test unfortunately doesn't work with FDCAN (due at least to filters being different), but I did try to modify it to work with FDCAN and that helped me see the three show-stopper bugs described above; it might be worth extending this test to work with FDCAN, to prevent regressions in the future (I can share my changes but they are a bit messy)

One final point: the existing ports/stm32/pyb_can.c:pyb_can_print() code is broken when printing out auto_restart on FDCAN, it should just always print auto_restart=False because that feature isn't supported on FDCAN.

@projectgus
Copy link
Copy Markdown
Contributor Author

projectgus commented Mar 12, 2026

It looks like testing CAN2 properly is going to take a little while, so suggest bumping this PR to 1.29.

Sorry about tests/ports/stm32/can.py, I'd forgotten this test existed.

@chrismas9
Copy link
Copy Markdown
Contributor

@projectgus If you are testing CAN2 you may be interested in an issue I just found with my pyb.CAN test script. Creating an instance of CAN2 corrupts the settings of CAN1, probably because they share a hardware block. I wrote the test way back when I did the F413 port. F413 has three CAN ports. I create and initialise up to three instances in a loop. It works on 1.17, but not on 1.25, possibly since the introduction of FDCAN.

Normally I do hardware abstraction (instantiation) first, then inititialisation, so its not a problem in my applications, just the test code. I don't know if this is considered an issue. Should I raise an issue for pyb.CAN?

Minimum code demonstrating the problem. This should run on a pyboard.

from pyb import CAN
can1 = CAN(1, CAN.LOOPBACK)
can1.setfilter(0, CAN.LIST16, 0, (123, 124, 125, 126))  # set a filter to receive messages with id=123, 124, 125 and 126
can2 = CAN(2, CAN.LOOPBACK)
can2.setfilter(0, CAN.LIST16, 0, (123, 124, 125, 126))  # set a filter to receive messages with id=123, 124, 125 and 126

can1.send('message1', 123)   	# send a message with id 123
print(can1.recv(0, timeout=50)) # receive message on FIFO 0
can2.send('message2', 123)   	# send a message with id 123
print(can2.recv(0, timeout=50)) # receive message on FIFO 0

Result

Traceback (most recent call last):
  File "<stdin>", line 8, in <module>
OSError: [Errno 110] ETIMEDOUT

Moving can1.setfilter fixes it.

can1 = CAN(1, CAN.LOOPBACK)
can2 = CAN(2, CAN.LOOPBACK)
can1.setfilter(0, CAN.LIST16, 0, (123, 124, 125, 126))  # set a filter to receive messages with id=123, 124, 125 and 126
can2.setfilter(0, CAN.LIST16, 0, (123, 124, 125, 126))  # set a filter to receive messages with id=123, 124, 125 and 126
(123, False, False, 0, b'message1')
(123, False, False, 0, b'message2')

@dpgeorge
Copy link
Copy Markdown
Member

@chrismas9 thanks for the report. I think we should open a separate issue for that, because it's not related to this PR. Are you able to do that?

@chrismas9
Copy link
Copy Markdown
Contributor

I have raised an issue for pyb.CAN. I only raised it here because machine.CAN maybe should be tested for the same issue.

#18922 STM32 pyb.CAN: Intantiating second instance corrupts settings of first instance.

Simplifies the pattern of an optional arg which can be a list of at
least a certain length, otherwise one is lazily initialised.

Modify pyb.CAN and ESP-NOW APIs to use the helper. Note this changes
the return type of pyb.CAN.recv() from tuple to list.

This work was funded through GitHub Sponsors.

Signed-off-by: Angus Gratton <[email protected]>
This work was funded through GitHub Sponsors.

Signed-off-by: Angus Gratton <[email protected]>
@projectgus projectgus force-pushed the feature/machine_can branch 2 times, most recently from f69842e to 2332712 Compare March 19, 2026 06:19
API is different to the original machine.CAN proposal, as numerous
shortcomings were found during initial implementation.

This work was funded through GitHub Sponsors.

Signed-off-by: Angus Gratton <[email protected]>
These are oddly missing from the STM32G4 HAL, but the
reference manual describes being able to use them and
the implementations seem to work as expected.

Note that unlike STM32H7 it doesn't seem like we must use this approach,
because HAL_FDCAN_AddMessageToTxFifoQ() does seem to not have the issues
with priority inversion seen on the H7. However it's simpler to use the
same API for both...

Signed-off-by: Angus Gratton <[email protected]>
Signed-off-by: Angus Gratton <[email protected]>
Implemented according to API docs in a parent comment.

Adds new multi_extmod/machine_can_* tests which pass when testing between
NUCLEO_G474RE, NUCLEO_H723ZG and PYBDV11.

This work was mostly funded through GitHub Sponsors.

Signed-off-by: Angus Gratton <[email protected]>
@projectgus
Copy link
Copy Markdown
Contributor Author

I think this is ready again. I believe I've addressed everything, plus re-tested - including the pyb.CAN unit tests with the changes from #18946 included, and a newly added unit test for machine.CAN(2). Have updated the PR description with specifics of all tests run.

One final point: the existing ports/stm32/pyb_can.c:pyb_can_print() code is broken when printing out auto_restart on FDCAN, it should just always print auto_restart=False because that feature isn't supported on FDCAN.

Have put this in its own PR: #18951

Copy link
Copy Markdown
Member

@dpgeorge dpgeorge left a comment

Choose a reason for hiding this comment

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

Thanks for updating and addressing all the feedback. Much appreciated.

I tested on:

  • PYBV10 standalone (tests bxCAN, CAN1 and CAN2)
  • PYBD_SF2 standalone (tests bxCAN, CAN1 only)
  • NUCLEO_H723ZG (tests FDCAD, CAN1 only)
  • PYBV10 - PYBV10 multi tests

Everything I tested passes.

@dpgeorge dpgeorge merged commit 6cac2d2 into micropython:master Mar 19, 2026
77 checks passed
@dpgeorge
Copy link
Copy Markdown
Member

🎉 🚀 🎉 🚀

@dpgeorge
Copy link
Copy Markdown
Member

Hats off to @projectgus for working on this for so long and seeing it through. Doing the initial docs draft, the common bindings, stm32 refactoring and implementation (for both bxCAN and FDCAN peripherals), extensive tests and testing, and final docs. A lot of work went into this.

@projectgus
Copy link
Copy Markdown
Contributor Author

Hurrah, thanks @dpgeorge. Let's hope it goes to good use!

@projectgus projectgus deleted the feature/machine_can branch March 20, 2026 01:09
@chrismas9
Copy link
Copy Markdown
Contributor

Let's hope it goes to good use!

It will. I have an automotive / industrial control project coming up that will put it to good use.

#define MICROPY_PY_MACHINE_BITSTREAM (1)
#endif
#ifndef MICROPY_PY_MACHINE_CAN
#ifdef MICROPY_HW_CAN1_TX
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

@projectgus @dpgeorge Is there any reason why this is not enabled for CAN2_TX too? I have boards that use CAN2, and I just added a || defined(CAN2_TX), am I missing something?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I think originally the thought was that if the board has two CAN peripherals then both CAN1 and CAN2 will be enabled.

But I guess it's possible that only CAN2 is enabled. So changing as you suggest makes sense.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

extmod Relates to extmod/ directory in source port-stm32

Projects

None yet

Development

Successfully merging this pull request may close these issues.

New machine.CAN driver

7 participants