Skip to content

stm32: Fix Classic CAN issue where initialising CAN1 corrupts CAN2. - #18992

Merged
dpgeorge merged 3 commits into
micropython:masterfrom
projectgus:bugfix/stm32_multi_can
May 14, 2026
Merged

dpgeorge merged 3 commits into
micropython:masterfrom
projectgus:bugfix/stm32_multi_can

Conversation

@projectgus

@projectgus projectgus commented Mar 25, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Testing

  • Ran the updated unit tests on a PYBV11 (Classic CAN) and a STM32G4 (FDCAN). PYBV11 fails without this fix, passes with this fix.
  • Re-run ports/stm32/pyb_can*.py extmod_hardware/machine_can*.py unit tests, multi_extmod/machine_can_*.py multi_pyb_can/*.py multi-tests.
  • RAN 3x CAN tests on NUCLEO-F413ZH board.

Trade-offs and Alternatives

  • The way pyb.CAN handles the CAN1/CAN2 filter split is a bit awkward, but as we want to focus on machine.CAN it's probably not worth spending more time thinking about it.

Generative AI

I did not use generative AI tools when creating this PR.

@projectgus

Copy link
Copy Markdown
Contributor Author

@chrismas9 if you get a chance then I'd be interested to know if the new unit tests added here pass on your boards (can run with mpremote run ... if you don't want to use the whole run-tests.py runner.)

Still waiting for an F413Z board to arrive so I can finish this.

@codecov

codecov Bot commented Mar 25, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 98.47%. Comparing base (21b3a51) to head (f16bb6f).
⚠️ Report is 100 commits behind head on master.

Additional details and impacted files
@@            Coverage Diff             @@
##           master   #18992      +/-   ##
==========================================
+ Coverage   98.46%   98.47%   +0.01%     
==========================================
  Files         176      176              
  Lines       22811    22845      +34     
==========================================
+ Hits        22460    22497      +37     
+ Misses        351      348       -3     

☔ 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.

@projectgus
projectgus force-pushed the bugfix/stm32_multi_can branch from eab0e62 to 1078d7d Compare March 25, 2026 00:08
@github-actions

github-actions Bot commented Mar 25, 2026 •

Copy link
Copy Markdown

Code size report:

Reference:  stm32/main: Init network stack before boot.py. [7d77d3e]
Comparison: tests/stm32/pyb_can: Update test for boards with CAN(3). [merge of f16bb6f]
  mpy-cross:    +0 +0.000% 
   bare-arm:    +0 +0.000% 
minimal x86:    +0 +0.000% 
   unix x64:    +0 +0.000% standard
      stm32:   +60 +0.015% PYBV10
      esp32:    +0 +0.000% ESP32_GENERIC
     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

@projectgus
projectgus force-pushed the bugfix/stm32_multi_can branch from 1078d7d to 6137e28 Compare March 25, 2026 00:40
@chrismas9

Copy link
Copy Markdown
Contributor

@projectgus pyb_can_instances.py fails on NUCEO_F413ZH, but passes on PYBv1.0. F413 fails three times with old build and two times with latest build. Results below. It's interesting that [1.2] passes on PYB, but fails on F413, even when CAN3 in not used.

I hope I built it properly. I haven't done this before. I cloned your repo, switched to bugfix/stm32_multi_can, make submodules and built PYBv1.0 and NUCLEO_F413HZ.

Tets results - MASTER

MPY: sync filesystems
MPY: soft reboot
test_controller_pairs (__main__.Test) ...testing config A with controller CAN(1, CAN.LOOPBACK, auto_restart=False)
received (123, False, False, 0, b'message1')
testing config B with controller CAN(2, CAN.LOOPBACK, auto_restart=False)
received (3, False, False, 0, b'message2')
testing config A with controller CAN(2, CAN.LOOPBACK, auto_restart=False)
testing config A with controller CAN(1, CAN.LOOPBACK, auto_restart=False)
testing config A with controller CAN(3, CAN.LOOPBACK, auto_restart=False)
received (123, False, False, 0, b'message1')
testing config B with controller CAN(1, CAN.LOOPBACK, auto_restart=False)
received (3, False, False, 0, b'message2')
testing config A with controller CAN(2, CAN.LOOPBACK, auto_restart=False)
testing config A with controller CAN(3, CAN.LOOPBACK, auto_restart=False)
received (123, False, False, 0, b'message1')
testing config B with controller CAN(2, CAN.LOOPBACK, auto_restart=False)
received (3, False, False, 0, b'message2')
 FAIL

======================================================================
FAIL: test_controller_pairs (__main__.Test)  [Testing CAN pair]  (id_a=2, id_b=1)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "<stdin>", line 77, in test_controller_pairs
  File "unittest/__init__.py", line 92, in fail
AssertionError: no rx!

======================================================================
FAIL: test_controller_pairs (__main__.Test)  [Testing CAN pair]  (id_a=1, id_b=3)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "<stdin>", line 77, in test_controller_pairs
  File "unittest/__init__.py", line 92, in fail
AssertionError: no rx!

======================================================================
FAIL: test_controller_pairs (__main__.Test)  [Testing CAN pair]  (id_a=2, id_b=3)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "<stdin>", line 77, in test_controller_pairs
  File "unittest/__init__.py", line 92, in fail
AssertionError: no rx!

----------------------------------------------------------------------
Ran 1 tests

FAILED (failures=3, errors=0)
>>> 

###############################################################################################################################
MicroPython v1.18-5289.g6137e28f3e on 2026-03-26; NUCLEO-F413ZH with STM32F413


MPY: sync filesystems
MPY: soft reboot
test_controller_pairs (__main__.Test) ...testing config A with controller CAN(1, CAN.LOOPBACK, auto_restart=False)
testing config A with controller CAN(2, CAN.LOOPBACK, auto_restart=False)
received [123, False, False, 0, b'message1']
testing config B with controller CAN(1, CAN.LOOPBACK, auto_restart=False)
received [3, False, False, 0, b'message2']
testing config A with controller CAN(1, CAN.LOOPBACK, auto_restart=False)
testing config A with controller CAN(3, CAN.LOOPBACK, auto_restart=False)
received [123, False, False, 0, b'message1']
testing config B with controller CAN(1, CAN.LOOPBACK, auto_restart=False)
received [3, False, False, 0, b'message2']
testing config A with controller CAN(2, CAN.LOOPBACK, auto_restart=False)
received [123, False, False, 0, b'message1']
testing config B with controller CAN(3, CAN.LOOPBACK, auto_restart=False)
received [3, False, False, 0, b'message2']
testing config A with controller CAN(3, CAN.LOOPBACK, auto_restart=False)
received [123, False, False, 0, b'message1']
testing config B with controller CAN(2, CAN.LOOPBACK, auto_restart=False)
received [3, False, False, 0, b'message2']
 FAIL

======================================================================
FAIL: test_controller_pairs (__main__.Test)  [Testing CAN pair]  (id_a=1, id_b=2)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "<stdin>", line 77, in test_controller_pairs
  File "unittest/__init__.py", line 92, in fail
AssertionError: no rx!

======================================================================
FAIL: test_controller_pairs (__main__.Test)  [Testing CAN pair]  (id_a=1, id_b=3)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "<stdin>", line 77, in test_controller_pairs
  File "unittest/__init__.py", line 92, in fail
AssertionError: no rx!

----------------------------------------------------------------------
Ran 1 tests

FAILED (failures=2, errors=0)
>>> 

My tests:
###############################################################################################################################
MicroPython v1.18-5289.g6137e28f3e on 2026-03-26; NUCLEO-F413ZH with STM32F413

------------------------------------------------------------------------------------------------------------------------
Test order:
 [[1, CAN(1, CAN.LOOPBACK, auto_restart=False)], [2, CAN(2, CAN.LOOPBACK, auto_restart=False)]]
------------------------------------------------------------------------------------------------------------------------
can1 receive failed
can2 [123, False, False, 0, b'can2']
------------------------------------------------------------------------------------------------------------------------
Test order:
 [[2, CAN(2, CAN.LOOPBACK, auto_restart=False)], [1, CAN(1, CAN.LOOPBACK, auto_restart=False)]]
------------------------------------------------------------------------------------------------------------------------
can2 [123, False, False, 0, b'can2']
can1 [123, False, False, 0, b'can1']
------------------------------------------------------------------------------------------------------------------------
Test order:
 [[1, CAN(1, CAN.LOOPBACK, auto_restart=False)], [3, CAN(3, CAN.LOOPBACK, auto_restart=False)]]
------------------------------------------------------------------------------------------------------------------------
can1 receive failed
can3 [123, False, False, 0, b'can3']
------------------------------------------------------------------------------------------------------------------------
Test order:
 [[3, CAN(3, CAN.LOOPBACK, auto_restart=False)], [1, CAN(1, CAN.LOOPBACK, auto_restart=False)]]
------------------------------------------------------------------------------------------------------------------------
can3 [123, False, False, 0, b'can3']
can1 [123, False, False, 0, b'can1']
------------------------------------------------------------------------------------------------------------------------
Test order:
 [[2, CAN(2, CAN.LOOPBACK, auto_restart=False)], [3, CAN(3, CAN.LOOPBACK, auto_restart=False)]]
------------------------------------------------------------------------------------------------------------------------
can2 [123, False, False, 0, b'can2']
can3 [123, False, False, 0, b'can3']
------------------------------------------------------------------------------------------------------------------------
Test order:
 [[3, CAN(3, CAN.LOOPBACK, auto_restart=False)], [2, CAN(2, CAN.LOOPBACK, auto_restart=False)]]
------------------------------------------------------------------------------------------------------------------------
can3 [123, False, False, 0, b'can3']
can2 [123, False, False, 0, b'can2']

###########################################################################################################
MicroPython v1.18-5289.g6137e28f3e on 2026-03-26; PYBv1.0 with STM32F405RG

MPY: sync filesystems
MPY: soft reboot
test_controller_pairs (__main__.Test) ...testing config A with controller CAN(1, CAN.LOOPBACK, auto_restart=False)
received [123, False, False, 0, b'message1']
testing config B with controller CAN(2, CAN.LOOPBACK, auto_restart=False)
received [3, False, False, 0, b'message2']
testing config A with controller CAN(2, CAN.LOOPBACK, auto_restart=False)
received [123, False, False, 0, b'message1']
testing config B with controller CAN(1, CAN.LOOPBACK, auto_restart=False)
received [3, False, False, 0, b'message2']
 ok
----------------------------------------------------------------------
Ran 1 tests

OK

@projectgus

Copy link
Copy Markdown
Contributor Author

Huh, thanks @chrismas9 that's interesting. Good that the results from your test code and the new unit test seem to agree that the problem on F413 is orderings 1,2 and 1,3.

My F413 board should arrive soon, so I'll probably hold off on looking at this further until then.

@chrismas9

Copy link
Copy Markdown
Contributor

I have three CANbus drivers I can connect to the F413. I can do an external CAN test when you are ready. It sends on each port and listens on th eother two.

@projectgus
projectgus force-pushed the bugfix/stm32_multi_can branch from 6137e28 to 4890c6c Compare April 23, 2026 00:36
@projectgus
projectgus marked this pull request as ready for review April 23, 2026 00:44
@projectgus

projectgus commented Apr 23, 2026 •

Copy link
Copy Markdown
Contributor Author

Received my NUCLEO-F413ZH board, and I think CAN3 is fixed now. @chrismas9 if you get a chance to confirm then it'd be much appreciated, thank you.

The problem was that the init path called can_clearfilter() in a loop to clear all pre-existing filters, but the self->can structure is not initialised at this point so the HAL chose CAN1 always. This works correctly for CAN1 and CAN2 as they share the filter space, but meant that CAN3 init would clobber the CAN1 filters.

All CAN unit & multi-tests now passing, except one: tests/ports/stm32/pyb_can.py fails on the NUCLEO-F413ZH on both master branch and this branch. The problem is in the "Test Filters" stage (marked with a print statement in this PR). From some experiments it seems like setting the same filter index twice without a deinit puts the peripheral in a bad state - and it stays in the bad state even after later deinit/init happens. 😮 This doesn't fail on other classic CAN MCUs (for example PYBDV11 with STM32F405RG this test passes).

It feels like a chip errata or a HAL bug but I can't see a "smoking gun" to confirm. Not fixing here as it's pre-existing issue that's not made worse by this fix.

Comment thread tests/extmod_hardware/machine_can_instances.py Outdated
@projectgus
projectgus force-pushed the bugfix/stm32_multi_can branch from 4890c6c to 505b16f Compare May 7, 2026 02:23
@dpgeorge

dpgeorge commented May 7, 2026

Copy link
Copy Markdown
Member

@chrismas9 will you get a chance to test this PR again? If not just let us know.

@chrismas9

Copy link
Copy Markdown
Contributor

For the 12 combinations of 2 or 3 CAN instances initialised in every order using my internal loopback test I get:

Master 7/12 fail.
This PR all pass.

I will set up external CAN buffers and run tests between channels in the next few days.

@chrismas9

Copy link
Copy Markdown
Contributor

The first test was with pyb.CAN in LOOPBACK.
I miscounted. 8/12 failures on Master, none withthis PR.

I have run the same 12 tests with external buffers with pyb.CAN and machine.CAN with no failures. The test sends on one channel and receives on all other channels.

for pyb.CAN

from pyb import CAN

# List up to 3 CAN channels in any order. Unavailable channels will be ignored.
# They will be instantiated and initialised in the order listed.
test_cans = [1,2,3]

cans = []
print('------------------------------------------------------------------------------------------------------------------------')
j = 0
for i in test_cans:
    cans.append([i,None])
    try:
        cans[j][1] = CAN(i, CAN.NORMAL)
        cans[j][1].setfilter(0, CAN.LIST16, 0, (123, 124, 125, 126))
    except:
        print('can' + str(i), 'not available')
    j +=1
print('Test order:\n', cans)

print('------------------------------------------------------------------------------------------------------------------------')

for i in range(0, j):
    can_msg = 'can' + str(cans[i][0]) + '->'
    if cans[i][1] is not None:
        try:
            cans[i][1].send(can_msg, 123)
            print('----------')
            try:
                for r in range(0, j):
                    if r != i:
                        print(can_msg, cans[r][0], cans[r][1].recv(0, timeout=50))
            except:
                print(can_msg, 'receive failed')
        except:
            print(can_msg, 'send failed')

for machine.CAN

from machine import CAN

# List up to 3 CAN channels in any order. Unavailable channels will be ignored.
# They will be instantiated and initialised in the order listed.
test_cans = [3,2]

cans = []
print('------------------------------------------------------------------------------------------------------------------------')
j = 0
for i in test_cans:
    cans.append([i,None])
    try:
        cans[j][1] = CAN(i, 500_000)
        cans[j][1].init(500_000, mode=CAN.MODE_NORMAL)
        cans[j][1].set_filters(((123, 0x7FF, 0), (124, 0x7FF, 0), (125, 0x7FF, 0), (126, 0x7FF, 0)))
    except:
        print('can' + str(i), 'not available')
    j +=1
print('Test order:\n', cans)

print('------------------------------------------------------------------------------------------------------------------------')

for i in range(0, j):
    can_msg = 'can' + str(cans[i][0]) + '->'
    if cans[i][1] is not None:
        try:
            cans[i][1].send(123, can_msg, 0)
            print('----------')
            try:
                for r in range(0, j):
                    if r != i:
                        res = cans[r][1].recv()
                        can_id, data, flags, errs = res
                        print(can_msg, cans[r][0], bytes(data))
            except:
                print(can_msg, 'receive failed')
        except:
            print(can_msg, 'send failed')

@projectgus

Copy link
Copy Markdown
Contributor Author

@chrismas9 Thanks, appreciate you independently confirming this!

@projectgus projectgus added this to the release-1.29.0 milestone May 13, 2026
@dpgeorge

Copy link
Copy Markdown
Member

@projectgus is this ready for final review/merging?

@projectgus

Copy link
Copy Markdown
Contributor Author

@projectgus is this ready for final review/merging?

I think so, yes

Comment thread tests/extmod_hardware/machine_can_instances.py Outdated
Comment thread tests/ports/stm32/pyb_can_instances.py Outdated
- CAN1 init would clear all filters including CAN2 filter range.
- CAN3 init would call can_clearfilter() with empty self->can values,
  the HAL layer interpreted this as clearing all CAN1 filters.

To fix this clearing filter banks is moved to deinit, so they're already
clean before the next init (they should be clear on initial init, due to
peripheral reset).

The only corner case is that if you initialise CAN1 and set too many
filters, initialise CAN2, then some of the CAN1 filters may now apply to
CAN2. However this would not have worked correctly in the current version
either (the extra CAN1 filters would have been silently cleared).

Includes expanded unit tests to cover arbitrary pairs of CAN instances.

This work was funded through GitHub Sponsors.

Signed-off-by: Angus Gratton <[email protected]>
This turned out not to be needed for the bugfix in previous commit,
but seems like a good practice anyway.

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 bugfix/stm32_multi_can branch from 505b16f to f16bb6f Compare May 14, 2026 00:45
@projectgus

Copy link
Copy Markdown
Contributor Author

@dpgeorge Thanks for the review! I've rewritten both tests to count the number of received messages and then assert it's equal to 1, which is a lot less fiddly. Re-ran on the same set of boards, all still passing.

@dpgeorge dpgeorge left a comment

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.

Looks good now, thanks for updating!

I did some basic tests on PYBV10 and everything passed.

@dpgeorge
dpgeorge merged commit 6574d98 into micropython:master May 14, 2026
41 checks passed
@projectgus
projectgus deleted the bugfix/stm32_multi_can branch May 14, 2026 02:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

STM32 pyb.CAN: Instantiating one CAN instance can corrupts settings of another instance.

3 participants