adafruit / Adafruit_CircuitPython_MCP2515

A CircuitPython library for working with the MCP2515 CAN bus controller
MIT License
21 stars 14 forks source link

Must have another device read the bus. #21

Open HourGlss opened 1 year ago

HourGlss commented 1 year ago

This was killing me to debug.

I started in this configuration: No Pican installed on CAN bus

This is the code for the two feathers:

SENDING

# SPDX-FileCopyrightText: Copyright (c) 2020 Bryan Siepert for Adafruit Industries
#
# SPDX-License-Identifier: MIT
from time import sleep
import board
import busio
from digitalio import DigitalInOut
from adafruit_mcp2515.canio import Message, RemoteTransmissionRequest
from adafruit_mcp2515 import MCP2515 as CAN
from canmessage import CanMessage

cs = DigitalInOut(board.CAN_CS)
cs.switch_to_output()
spi = busio.SPI(board.SCK, board.MOSI, board.MISO)

can_bus = CAN(spi, cs,baudrate=1000000)
cm = CanMessage()
cm.next()
cm.reset()
while True:
    with can_bus.listen() as listener:
        cm.next()
        message = Message(cm.id, data=cm.data)
        cm.reset()
        send_success = can_bus.send(message)
        print("Send success:", send_success)
        print(f"Send details: id{message.id} d{message.data}")
        message_count = listener.in_waiting()
        print(message_count, "messages available")
        for _i in range(message_count):
            msg = listener.receive()
            print("Message from ", hex(msg.id))
            if isinstance(msg, Message):
                print("message data:", msg.data)
            if isinstance(msg, RemoteTransmissionRequest):
                print("RTR length:", msg.length)
    sleep(1)

RECEIVE ONLY:

import digitalio
import board
import busio
from adafruit_mcp2515.canio import RemoteTransmissionRequest, Message
from adafruit_mcp2515 import MCP2515 as CAN

cs = digitalio.DigitalInOut(board.CAN_CS)
cs.switch_to_output()
spi = busio.SPI(board.SCK, board.MOSI, board.MISO)
mcp = CAN(spi, cs, silent=True,baudrate=1000000)
mcp.restart()
neo_on = False
next_message = None
message_num = 0
while True:
    # print occationally to show we're alive
    with mcp.listen(timeout=1) as listener:
        message_count = mcp.unread_message_count
        if message_count == 0:
            continue
        print(message_count)
        next_message = mcp.read_message()
        message_num = 0
        while not next_message is None:
            message_num += 1

            msg = next_message
            print(f"mnum{message_num} ID:{hex(msg.id)}", end=",")
            if isinstance(msg, Message):
                if len(msg.data) > 0:
                    pass
                    print(f"Data:{msg.data}")
                    # message_str = ",".join(["0x{:02X}".format(i) for i in msg.data])
                    # print(message_str)

            if isinstance(msg, RemoteTransmissionRequest):
                print("RTR_LEN:", msg.length)
            next_message = mcp.read_message()

Without the pican hooked up this is what happens putty output from send (left) // receive (right)

Connecting the pican and having it read the messages from the bus. works as intended photo of pican attached

What the pican is doing is simply reading the messages from the bus. without the pican attached the example programs provided on https://github.com/adafruit/Adafruit_CircuitPython_MCP2515/tree/1.1.2/examples do not work at any baudrate. With the pican attached and reading from the bus they work at 1000000 baud.

anecdata commented 1 year ago

Probably related to #10

Some discussion on Discord working backward from here.

I could be reading the datasheets wrong, but I think this may be intended behavior, though perhaps a more friendly exception could be raised. When there is only a sender on the bus, no acknowledgements are received and the bus goes from Active to Passive bus state. The send queue fills up.

Various docs and datasheets discuss this (p.9 of an Analog CAN datasheet):

Although the arbitration field is transmitted as recessive, the transmitting node checks for a dominant bit on the bus during this bit time. This dominant bit is an ACK bit sent by any node receiving the message correctly. A transmit error occurs if no other node acknowledges the message.

Unfortunately, the Microchip datasheet isn't as clear (to me anyway), but see also Section 4.4 in a TI CAN datasheet.

The bus state can be checked before sending to make sure that it is in Active state, signifying that a listener is successfully receiving messages.

can_bus = MCP2515(spi, cs)
listener = can_bus.listen()
print(can_bus.state)

edit: ah, here we go, in the Microchip MCP2515 datasheet (p.7):

The final field is the two-bit Acknowledge (ACK) field. During the ACK Slot bit, the transmitting node sends out a recessive bit. Any node that has received an error-free frame Acknowledges the correct reception of the frame by sending back a dominant bit (regardless of whether the node is configured to accept that specific message or not).

edit 2: Sorry, took me a while to get that the issue occurs with only a sender and a receiver and without a third device. That is unexpected. I have a setup with only a canio sender and MCP2515 receiver, and it's fine (my code is a little different). I'll put together another MCP2515-based device and do some testing.

HourGlss commented 1 year ago

so... im an idiot. how can we add that that last part to the mcp2515 module?

I can confirm that this code block

mcp = CAN(spi, cs, baudrate=1000000)
next_message = None
message_num = 0
while True:
    message_count = mcp.unread_message_count
    message_num = 0
    next_message = mcp.read_message()
    while next_message is not None:
        message_num += 1
        msg = next_message
        next_message = mcp.read_message()

does not cause mcp.unread_message_count to decrease. I don't believe the received messages are removed from the message buffer....

I can also confirm that this code

# SPDX-FileCopyrightText: Copyright (c) 2020 Bryan Siepert for Adafruit Industries
#
# SPDX-License-Identifier: MIT
from time import sleep
import board
import busio
from digitalio import DigitalInOut
from adafruit_mcp2515.canio import Message, RemoteTransmissionRequest
from adafruit_mcp2515 import MCP2515 as CAN
from canmessage import CanMessage

cs = DigitalInOut(board.CAN_CS)
cs.switch_to_output()
spi = busio.SPI(board.SCK, board.MOSI, board.MISO)

can_bus = CAN(spi, cs,baudrate=1000000)
cm = CanMessage()
cm.next()
cm.reset()
while True:
    with can_bus.listen() as listener:
        cm.next()
        message = Message(cm.id, data=cm.data)
        cm.reset()
        send_success = can_bus.send(message)
        print("Send success:", send_success)
        print(f"Send details: id{message.id} d{message.data}")
        message_count = listener.in_waiting()
        print(message_count, "messages available")
        for _i in range(message_count):
            msg = listener.receive()
            print("Message from ", hex(msg.id))
            if isinstance(msg, Message):
                print("message data:", msg.data)
            if isinstance(msg, RemoteTransmissionRequest):
                print("RTR length:", msg.length)
    sleep(1)

is unable to read the messages it puts on the bus if that helps.

anecdata commented 1 year ago

I just got two RP2040 Feathers, each with MCP2515 FeatherWings, to talk to each other with nothing else on the bus. A sends, B receives, B sends a CircuitPython-level code acknowledgement, A receives that acknowledgement before sending the next message. Maybe the code is sufficiently different? Not sure. My code is kind of a mess, in the middle of evolving it to add features. I'll try to pare it down (strip the acknowledgements and other fluff) and post code.

anecdata commented 1 year ago

I think this is the issue:

mcp = CAN(spi, cs, silent=True,baudrate=1000000)

In Silent mode, the receiver can't assert a dominant ACK, so sender thinks the bus has no other devices.

I think it needs to be in Loopback mode to read its own messages?

FWIW, here's the pared-down code, which works even if the receiver is interrupted by control-C (I think perhaps library should take device out of normal active mode before exiting).:

Receiver ```py import struct import time import board import busio import digitalio try: import canio NATIVE = True except ImportError: NATIVE = False import adafruit_mcp2515.canio as canio from adafruit_mcp2515 import MCP2515 # # CAN LISTENER # DEBUG = True RCVRID = 0x100 MSGTIMEOUT = 5 def can_recv_msg(): while True: msg = listener.receive() if (can_bus.transmit_error_count > 0) or (can_bus.receive_error_count > 0): print(f"🔴 MSG tx_err={can_bus.transmit_error_count} rx_err={can_bus.receive_error_count}") if msg is None: if DEBUG: print("🟡 MSG not received within timeout") continue if DEBUG: print(f"MSG {msg.data} from={hex(msg.id)}") if isinstance(msg, canio.Message): break else: if DEBUG: print("🟡 not a canio message") return msg time.sleep(3) # wait for serial print(f"{'='*25}") if NATIVE: can_bus = canio.CAN(rx=board.RX, tx=board.TX, baudrate=500_000, auto_restart=True) else: cs = digitalio.DigitalInOut(board.D5) cs.switch_to_output() spi = board.SPI() can_bus = MCP2515(spi, cs, baudrate=500_000) listener = can_bus.listen(timeout=MSGTIMEOUT) old_bus_state = None while True: bus_state = can_bus.state if bus_state != old_bus_state: print(f"🟣 BUS state changed to {bus_state}") old_bus_state = bus_state print(f"Receiving...") if DEBUG: print(f"MSG avail={listener.in_waiting()} unread={can_bus.unread_message_count}") msg = can_recv_msg() print(f"{'-'*25}") ```
Sender ```py import random import math import struct import time import board import busio import digitalio try: import canio NATIVE = True except ImportError: NATIVE = False import adafruit_mcp2515.canio as canio from adafruit_mcp2515 import MCP2515 # # CAN SENDER # DEBUG = False SENDERID = 0x400 def can_send_msg(data): message = canio.Message(id=SENDERID, data=data) can_bus.send(message) if (can_bus.transmit_error_count > 0) or (can_bus.receive_error_count > 0): print(f"🔴 MSG tx_err={can_bus.transmit_error_count} rx_err={can_bus.receive_error_count}") print(f"MSG {data} len={len(data)}") return time.sleep(3) # wait for serial print(f"{'='*25}") if NATIVE: can_bus = canio.CAN(rx=board.RX, tx=board.TX, baudrate=500_000, auto_restart=True) else: cs = digitalio.DigitalInOut(board.D5) cs.switch_to_output() spi = board.SPI() can_bus = MCP2515(spi, cs, baudrate=500_000) old_bus_state = None while True: bus_state = can_bus.state if bus_state != old_bus_state: print(f"🟣 BUS state changed to {bus_state}") old_bus_state = bus_state print(f"Sending...") data = "".join(random.choice("0123456789ABCDEF") for i in range(random.randint(1, 6))).encode() can_send_msg(data) print(f"{'-'*25}") time.sleep(1) ```
anecdata commented 1 year ago

Output of above code illustrates the failure mode, but it takes either a receiver hard reset (without the code running after), or going into REPL and manually putting the receiver can bus into silent mode:

-------------------------
Sending...
MSG b'952' len=3
-------------------------
Sending...
🔴 MSG tx_err=32 rx_err=0
MSG b'9C210' len=5
-------------------------
🟣 BUS state changed to 2
Sending...
🔴 MSG tx_err=128 rx_err=0
MSG b'FF' len=2
-------------------------
Sending...
🔴 MSG tx_err=128 rx_err=0
MSG b'505A1' len=5
-------------------------
Sending...
Traceback (most recent call last):
  File "code.py", line 53, in <module>
  File "code.py", line 26, in can_send_msg
  File "adafruit_mcp2515/__init__.py", line 398, in send
RuntimeError: No transmit buffer available to send

The MCP2515 has three send buffers. The first message accumulates enough on-chip send retry fails to put the bus into ERROR_PASSIVE = 2 state. The 2nd and 3rd messages also fail. All buffers are full, and the exception is raised.