Beware: Spark Max Duty Cycle Encoder "Refresh Rate"

The TLDR for this topic is quick and simple:
When possible, you should probably be running your PID loops within your motor controller

But there is a nice story behind the previous statement which lead to some interesting discoveries which I will share here.

1741’s students this past season decided to go with a swerve drivetrain, which wasn’t new terrain for us, but was certainly a rusty skill as it had been a few years and COVID since we last had a swerve bot. The students landed on the SDS MK4i modules and decided to use a Falcon500 for the drive motor and a NEO + SRX Mag Encoder for the turn motor.

At the beginning of the season, the goal was to run all of our drivetrain’s PID loops within the motor controllers. As we all know, the RoboRIO is slow (50 Hz) compared to the PID loops which are running within the motor controllers (1000 Hz).

For this reason, we had the students create a custom ribbon cable to feed the Mag Encoder’s absolute signal (duty cycle signal) into the Spark Max. “Gravy,” we thought, “we should be able to run a PID loop within the Spark Max.” And then… Gotcha!! At the time, the Spark Max firmware didn’t support the absolute encoder through the data port in the way that we needed. (Shoutout to REV, who just recently pushed a firmware update “1.5.0” to add this capability) “Whatever,” we thought, “we can just get the value from the Spark Max in our Java code, and run the PID loop there.” “50 Hz isn’t that bad… right?”

And so we did. It was exciting to watch the students discover the idiosyncrasies of FRC swerve as they got the bot up and running, but at the end of the day, the bot never quite felt right. I had driven a swerve bot when I was a student, and our 2023 bot Tempest didn’t pass the “good swerve sniff test” per se. It wasn’t for a lack of trying though… In the background, this drove us 1741 programming mentors a little crazy. At one point, I sat down and rederived the matrix algebra within WPILib’s swerve calculations to make sure there wasn’t some small mistake. No dice… everything looked correct, and everything that we tried didn’t really help the swerve drive to feel better. At this point, we gave up and accepted the state of the swerve drive.

Fast forward the story to now, in the offseason. I was doing some unrelated research into the FRC CAN bus to look at monitoring the bus and potentially controlling the brushless motors controllers via a Raspberry Pi. Regardless, I landed on this page of the Spark Max documentation which rocked my world:

The absolute encoder position was only being communicated to the RoboRIO every 200ms!! 5 Hz!!!

All it took was one line of code, and our swerve drive felt incomprehensibly better:

m_turnMotor.setPeriodicFrameRate(PeriodicFrame.kStatus5, 20);

This line of code forces the Spark Max to send the absolute encoder position to the RoboRIO every 20ms, which means that PID loop within the RIO will be able to pull up-to-date encoder values each cycle rather than only getting an updated value every 10 cycles.

I’m aware that this is a very niche issue (the perfect storm was required to cause this one), but I hope that you learned some stuff and don’t make the same mistake we made.

Do you have any related niche programming issues which plagued you this season?


Nice write up!

Whether you’re going through the RIO or through the data port, it’s probably best to use the relative output from the encoder for control, and use the absolute position to seed the relative encoder and the beginning of the match.


Always been a little afraid of this, as a reboot of the motor controller would be unrecoverable without some more complex logic. Right? Or is this not a concern.

The “restart robot code” button is always a failsafe there. There are cleaner ways, but that’s worst case. I’m pretty sure that’s our plan if an issue ever happened, but it never has for us.

Setting reasonable current limits on the steering motors also helps mitigate the risk.


If this is a concern, you can check CANSparkMax.getStickyFault(FaultID.kHasReset) to determine if it has rebooted since the last time sticky faults were cleared. (And then call clearFaults() to clear the fault.)

Just a note on this- while you’re never going to get to 1khz, you can absolutely run individual controllers faster than 50hz- TimedRobot has an addPeriodic function that allows additional functions to be called synchronously with, but at a different rate than, the main loop functions. We’ve had success running controllers at 100hz in the past (only the PID calculation and the subsequent motor set call were run at 100hz).


It’s worth mentioning that the SMAX data port can only use Alternate Encoder Mode here, which has a max RPM of 2400 with the SRX Mag. Not an issue in this application. The mag encoder only has a 250Hz PWM output though, so if you’re using motor controller onboard PID, it’s definitely better to use the relative output as you say.
With 50Hz update back to the RoboRIO, there’s no huge advantage either way.

1 Like

1741 hasn’t used this functionality yet, but I think we’re headed that direction. We’ve started setting up “PeriodicIO” structures for each of our subsystems with the intent that a subsystem can safely be on its own thread. Ideally we’d have a thread for each subsystem which is only writing to the CAN bus and running at the speed which is necessary. Maybe an arm can update less often than the drivetrain for example.

Does the Redux Canandcoder improve on that 250Hz read rate? I would assume it can’t because that 250Hz is associated with the 4098 microsecond pulse width.

Secondary PSA: Do NOT set your status frames within about 2 seconds of a burnFlash() or resetFactoryDefaults(). These will block communication and your set messages might not get through.


The Canandcoder has a 500Hz refresh rate. We thought this would strike a nice balance between the lower precision of the REV throughbore (1kHz, 10 bit) and the slower speed of the CTRE Mag (250Hz, 12 bit). Note that we do have higher precision than both at 14 bits, but there’s a small deadzone at the ends driven by the need to have a 1us deadzone at either end of the range (CTRE and REV also have this limitation).