Improved performance for mjcanfd-usb-1x

It seems that development of the mjcanfd-usb-1x and fdcanusb comes in spurts! After more than a year of silence, but shortly after announcing a new firmware with support for customizable sample points, we have a new firmware release with some exciting improvements, 2025-09-19 on github! The short is that the mjcanfd-usb-1x is now faster and able to successfully drive longer chains of controllers in a high performance pipelined mode. Read on for more details:

Automatic retransmission

The first change that was made was to enable can.automatic_retransmission by default. Two ways that a CAN-FD transmission can fail are if the transmitter loses arbitration or if no device on the bus acknowledges the frame.

First, let’s talk about arbitration. CAN-FD (and CAN 2.0) in the large is a multi-master bus, and multiple nodes on the bus can initiate a transmission at basically any time. You might ask, well, what happens if two devices decide to try and send at the same time? There is a delineated process call “arbitration”, where devices first listen for other transmitters before starting to transmit, and continue to listen for other transmitters while they are transmitting in the event that two devices start transmitting within the window of a single bit. If one or more devices are detected which conflict, the ones transmitting the lowest ID “win” the arbitration and get to keep going and all others stop.

Nominally, a CAN-FD network with moteus is not a multi-master bus, but has a single controller and many peripherals. If the bus is operated where the controller (aka the mjcanfd-usb-1x) always waits for a response before sending more commands, then no arbitration will ever be invoked. However, if you want to speed things along and send multiple command frames at once, then it is pretty easy for the mjcanfd-usb-1x to conflict with a moteus device that was responding to a previous command. Previously, if auto-retransmission was off, this frame would then just be lost and would appear as a missing response from the device, even though it was in fact a missing command from the controller!

The second idea, failure for no acknowledgment, makes sense, but can be problematic if no device will ever acknowledge the frame. In a functioning system, this normally shouldn’t happen, but during development there are all kinds of reasons why the user may accidentally send a frame that can not be acknowledged. With moteus, you might not know what the ID of a device is, or you may think a device is powered, when it is not yet powered. In that case, the mjcanfd-usb-1x would continue to transmit the frame indefinitely, never attempting any other frames that may actually have a recipient.

To make automatic transmission not really mess up the user experience in the case of missing recipients, as part of the new changes a heuristic is used. A configurable amount of time, 50ms by default, after the last time a frame was sent to the hardware fifo, all frames in the hardware FIFO are cancelled. This is very simple to implement and does not cause too much collateral damage in the even that it is actually invoked. The STM32G4 only has a 3 level hardware FIFO, so at most 2 unecessary frames would be lost, and in situations where unacknowledged frames are being sent, it is likely driven by the user where they are going to attempt something different later on at human time scales anyway.

Software FIFO

As just mentioned, the STM32G4 used in the mjcanfd-usb-1x has a 3 level hardware FIFO for transmission and reception. If the mjcanfd-usb-1x is used in the single device polling mode, this is not a problem at all, but once again if pipelining is used where many commands are sent without immediately waiting for responses this can FIFO limit can easily be exceeded.

So, as part of this new firmware version, additional software FIFO queues are included. For transmissions, there is a 32 frame queue. For receptions, the queue is not kept at the frame level, but at the protocol response level and is between 2k and 4k characters, which works out to typically between 20 and 40 frames of response depending upon the frames’ length.

Improved firmware performance

Finally, no real effort had been put into optimization of the firmware on the mjcanfd-usb-1x before. There were several instances where quite enormous amounts of work were being done per character of input received over USB and similarly for response messages. By spending a bit of time with the ctrl-c gdb profiler, I fixed up the worst cases of this. Thus the overall latency from when a USB command was sent to a frame was greatly reduced and also the latency from when a CAN-FD frame was received to when the response was sent to USB was decreased.

Performance measurements

The overall result is that pipelined operation with the mjcanfd-usb-1x is now feasible for busses of over 20 devices and the resulting command update rate is greatly improved. Here is a table constructed by using bandwidth_test.cc from a desktop PC running with chrt 99 on the old firmware and new firmware for different number of devices, along with a PEAK USB CAN-FD adapter connected using socketcan:

Device Count PEAK USB Hz Old mjcan Hz New mjcan Hz
1 700 800 1600
2 690 500 1270
3 640 340 1000
4 600 260 870
5 570 200 750
6 530 160 630
7 530 140 570
8 530 120 500
9 350 100 450
10 340 90 410

The overall results are much better than before, in some cases by 4x, and match or surpass the PEAK adapter at nearly every number of devices.

Updating

Unfortunately, the mjcanfd-usb-1x and fdcanusb do not have a USB bootloader capable of flashing new firmware by itself. To update the firmware you will need a standalone STM32 SWD programmer with appropriate ZH6 connector, like the one in the mjbots store and a linux system with OpenOCD installed.

sudo apt install openocd

works on Debian/Ubuntu.

Once that is ready, you can flash new firmware using the flash.py script in the fdcanusb repository.

./flash.py
path/to/20250919-fdcanusb-4c70208d37e341c8edd4360dd426c67721193e4e.elf