MA600 has 10x noise in half its range

I’m using a MA600 encoder. I’m seeing a dramatic increase in standard deviation noise depending on the angle. I’m seeing on the order of 0.01 degrees noise in the 0-180 range and 0.10 degrees noise in the 180-360 range. Almost (but not quite exactly?) at the 0 and 180 mark. This persists if I reverse the orientation of the breakout — the noise stays in the 180-360 reading range, not in the same physical range. It also persists if I use the functional test mode that bypasses the physical sensing altogether — noise at 270 degrees is about 10x the noise at 90 degrees.

I tested parity, and the noisy measurements have correct parity. I tested disabling the filter with no improvement. I got the MA600 on a very simple breakout from MJBots. I’ve got it connected to a seeed Studio Xiao RP2350 using 1 Mhz SPI. I’m programming that through the Arduino IDE, just logging measurements.

I have spreadsheets with captured data that I could share. It includes observations on each side of 0 degrees with their dramatically different noise. I’ll paste in an image of a plot of some data. This was captured at 1 kHz. The mean measurement was just 0.114 degrees apart (named before and after “bump”), but the noise profiles are immensely different. These are so close to 0 that the distribution of noise crosses the 0 line… but doesn’t magically become more or less noisy when it crosses. This pattern is very repeatable and also happens at 180 degrees.

I’m desperate for ideas.

Hello @jbonyun,

Could you provide more information about the setup?:

  1. Relative position between sensor and magnet.

  1. Magnet characteristics (form, magnetization direction, material and size)

Thanks for the reply.

The magnet is a disk, 6mm diameter, 2.5mm thick, N35, diametrically magnetized. Literally this one: Diametric Disc Magnet – mjbots Robotic Systems . It is on the end of a somewhat-ferromagnetic stainless steel shaft with 6mm diameter. The shaft goes through a bearing just 0.5mm before the magnet. I know this isn’t ideal, and a spacer is in my plans to give it a gap from those.


(Magnet on end of shaft just outside of bearing.)

The magnet is on the shaft axis. The sensor is on the shaft axis. There is a 2mm air gap between the sensor (the top side of the package) and the magnet. The sensor is part of this breakout: MA600 Breakout – mjbots Robotic Systems.


(Sensor IC through spacer.)


(CAD cross-section. Magnet sticking out from the top of the gap, sensor IC sticking out from the bottom of the gap.)

I’ve been playing with different FW filter window. My original data is on FW=5. Making it larger will reduce noise everywhere, but there remains a 10x difference between the two halves of the circle.

Does that give you any ideas?

Thanks!

The setup looks good (30mT on sensor according to simulation).

There could be some setup imperfections that might affect the sensor performance, could you check:

  1. If there is Ferromagnetic components around the sensor
  2. If there is Eccentricity: Check out if the sensor is perfectly aligned with the shaft rotation axis.

Could you also repeat the measures changing the sensor zero setting (registers 0 and 1)? The idea is to check if the noise increase moves with the output angle or if it stays at the same mechanical position.

It took me a couple of days to get to it. The problem persists.

I made a new fixture that would let me put a 12.5mm plastic spacer from the end of the stainless shaft to the magnet. That’s about 15mm from the bearing. The noise is about the same.

Here are the histograms on both sides of 0 and both sides of 180:

It’s hard to shift the magnet relative to the axis with the spacer. I’ve done my best to center it. But in the previous tests (before the spacer), I has able to push the magnet to slightly different positions, and it didn’t matter. I just don’t think that’s what’s going wrong.

Finally, I tried your suggested zero-setting test. It also shows bad results. Essentially the noise follows the reported numbers, even when the magnet hasn’t moved. Here is a table of the results. It also works the reverse if I start between 0 and 180 (can’t post a 4th image, apparently).

I’ll include the Arduino sketch that I’m using for this last one, in case you spot anything stupid I’m doing. I can’t attach it as a file, so it’s just below. (Yes, it is vibe coded.)

Maybe it’s time I get a new MA600 breakout. I’ve ordered a MA600A (not MA600 this time) reference board from MPS. I’ll also order a new kind of microcontroller. I’m working on getting a SAMD21 board.

Any other ideas?

// Adjusts the zero-setting in the registers, to see if that moves where the noise is.
// MPS asked me to try this.

// MA600 / MA600A zero-offset A/B/A test. XIAO RP2350, Arduino-Pico core.
// CS D7, SCK D8, MISO D9, MOSI D10. Same wiring as previous sketches.
// s: ~20 Hz stream; b: ONE original/shifted/restored experiment; d: export.
// Hold the magnet and board stationary throughout all three stages.
// FW is explicitly set to 5 at startup and remains 5. Only volatile writes.
// Registers 0/1 are restored after the experiment, including on detected failure.
// No NVM store commands. If interrupted, power-cycle SENSOR and MCU together.

#include <Arduino.h>
#include <SPI.h>
#include <math.h>
#include <stdio.h>

constexpr uint8_t ENCODER_CS = D7;
constexpr uint8_t FILTER_FW = 5;
constexpr uint32_t SAMPLE_HZ = 1000;
constexpr unsigned SAMPLE_COUNT = 2000;
constexpr uint32_t SETTLE_MS = 3000;
constexpr uint16_t ZERO_SHIFT_COUNTS = 32768; // 180 degrees
constexpr uint32_t PERIOD_US = 1000000UL / SAMPLE_HZ;
constexpr double DEG_PER_COUNT = 360.0 / 65536.0;
static_assert(SAMPLE_HZ > 0 && SAMPLE_HZ <= 20000 && 1000000UL % SAMPLE_HZ == 0,
“Choose a rate <=20 kHz that divides 1000000 exactly.”);
static_assert(SAMPLE_COUNT >= 2 && SAMPLE_COUNT <= 10000, “Use 2..10000 samples.”);
SPISettings encoderSPI(1000000, MSBFIRST, SPI_MODE0);

struct Capture {
uint16_t raw[SAMPLE_COUNT];
uint32_t timeUs[SAMPLE_COUNT];
uint16_t zero;
uint32_t missed, maxLate;
uint8_t status;
bool valid;
};
Capture captures[3];
const char* const stageNames[3] = {“ORIGINAL_BEFORE”, “ZERO_SHIFT_180”, “ORIGINAL_AFTER”};
uint8_t expectedFilter = 5;
uint32_t experiment = 0, lastStream = 0;
bool ready = false, streaming = true;

uint16_t frame(uint16_t command) {
SPI.beginTransaction(encoderSPI);
digitalWrite(ENCODER_CS, LOW);
delayMicroseconds(1);
uint16_t value = SPI.transfer16(command);
delayMicroseconds(1);
digitalWrite(ENCODER_CS, HIGH);
SPI.endTransaction();
delayMicroseconds(5);
return value;
}

uint8_t readRegister(uint8_t address) {
frame(0xD200u | address);
return static_cast<uint8_t>(frame(0));
}

bool writeRegister(uint8_t address, uint8_t value) {
frame(0xEA54);
frame((static_cast<uint16_t>(address) << 8) | value);
uint8_t echo = static_cast<uint8_t>(frame(0));
delay(1);
uint8_t actual = readRegister(address);
bool ok = echo == value && actual == value;
char line[120];
snprintf(line, sizeof(line), “REG%u requested=0x%02X echo=0x%02X readback=0x%02X %s”,
unsigned(address), unsigned(value), unsigned(echo), unsigned(actual), ok ? “OK” : “FAILED”);
Serial.println(line);
return ok;
}

uint16_t readZero() {
uint8_t low = readRegister(0);
uint8_t high = readRegister(1);
return (static_cast<uint16_t>(high) << 8) | low;
}

bool setZero(uint16_t zero) {
// Do not short-circuit: attempt both writes, including during restoration.
bool lowOK = writeRegister(0, static_cast<uint8_t>(zero));
bool highOK = writeRegister(1, static_cast<uint8_t>(zero >> 8));
return lowOK && highOK && readZero() == zero;
}

bool verify(uint16_t zero) {
return readZero() == zero && readRegister(13) == expectedFilter && readRegister(28) == 0;
}

int32_t difference(uint16_t raw, uint16_t anchor) {
int32_t d = static_cast<int32_t>(raw) - anchor;
if (d > 32767) d -= 65536;
if (d < -32768) d += 65536;
return d;
}

bool captureStage(unsigned stage, uint16_t zero) {
Capture& c = captures[stage];
c.valid = false;
c.zero = zero;
c.missed = c.maxLate = 0;
Serial.print(“STAGE “); Serial.print(stageNames[stage]);
Serial.println(” – settling 3 seconds; do not touch anything.”);
Serial.flush();
delay(SETTLE_MS);
if (!verify(zero)) {
Serial.println(“ABORT: configuration verification failed.”);
return false;
}
frame(0); // Discard first angle after register reads.
delay(100);
uint32_t deadline = micros();
SPI.beginTransaction(encoderSPI);
for (unsigned i = 0; i < SAMPLE_COUNT; ++i) {
uint32_t now;
do { now = micros(); } while (static_cast<int32_t>(now - deadline) < 0);
uint32_t late = now - deadline;
if (late > c.maxLate) c.maxLate = late;
if (late >= PERIOD_US) {
c.missed += late / PERIOD_US;
deadline = now; // Avoid a rapid catch-up burst.
}
c.timeUs[i] = micros(); // Host CS timestamp, not sensor conversion time.
digitalWrite(ENCODER_CS, LOW);
delayMicroseconds(1);
c.raw[i] = SPI.transfer16(0);
delayMicroseconds(1);
digitalWrite(ENCODER_CS, HIGH);
deadline += PERIOD_US;
}
SPI.endTransaction();
delayMicroseconds(5);
c.status = readRegister(26);
c.valid = verify(zero);
if (!c.valid) Serial.println(“ABORT: configuration changed during capture.”);
return c.valid;
}

void summary(unsigned stage) {
const Capture& c = captures[stage];
if (!c.valid) return;
double mean = 0, m2 = 0;
int32_t low = 0, high = 0;
uint32_t minDt = UINT32_MAX, maxDt = 0;
for (unsigned i = 0; i < SAMPLE_COUNT; ++i) {
int32_t x = difference(c.raw[i], c.raw[0]);
if (x < low) low = x;
if (x > high) high = x;
double d = x - mean;
mean += d / (i + 1);
m2 += d * (x - mean);
if (i) {
uint32_t dt = c.timeUs[i] - c.timeUs[i-1];
if (dt < minDt) minDt = dt;
if (dt > maxDt) maxDt = dt;
}
}
double angle = fmod((c.raw[0] + mean) * DEG_PER_COUNT + 360.0, 360.0);
uint32_t span = c.timeUs[SAMPLE_COUNT-1] - c.timeUs[0];
char line[340];
snprintf(line, sizeof(line),
“SUMMARY experiment=%lu stage=%s fw=%u zero=0x%04X zero_deg=%.6f n=%u mean_deg=%.6f sd_deg=%.6f pp_deg=%.6f status26=0x%02X”,
(unsigned long)experiment, stageNames[stage], unsigned(FILTER_FW), unsigned(c.zero),
c.zero * DEG_PER_COUNT, SAMPLE_COUNT, angle, sqrt(m2/(SAMPLE_COUNT-1))*DEG_PER_COUNT,
(high-low)*DEG_PER_COUNT, unsigned(c.status));
Serial.println(line);
snprintf(line, sizeof(line),
“TIMING stage=%s actual_hz=%.3f min_interval_us=%lu max_interval_us=%lu max_lateness_us=%lu missed_slots=%lu”,
stageNames[stage], span ? (SAMPLE_COUNT-1)*1000000.0/span : 0,
(unsigned long)minDt, (unsigned long)maxDt, (unsigned long)c.maxLate, (unsigned long)c.missed);
Serial.println(line);
if (c.missed) Serial.println(“WARNING: timing was not uniform; repeat this experiment.”);
if (high-low >= 32768) Serial.println(“WARNING: excessive motion; stationary statistics are invalid.”);
}

void runExperiment() {
streaming = false;
if (!ready) { Serial.println(“Sensor not ready; power-cycle sensor and MCU.”); return; }
uint16_t original = readZero();
if (!verify(original) || readZero() != original) {
Serial.println(“ABORT: unstable/unexpected configuration; no zero writes made.”); return;
}
++experiment;
for (unsigned j=0; j<3; ++j) captures[j].valid = false;
Serial.println(“\n=== ZERO SHIFT TEST BEGIN: FW5, MODE0, SPI 1MHz, direct 1kHz ===”);
Serial.println(“Keep rotor AND sensor stationary until DONE. No external averaging.”);
Serial.println(“SD uses n-1, with wrap-safe differences from each stage’s first sample.”);
bool ok = captureStage(0, original);
if (ok) ok = setZero(static_cast<uint16_t>(original + ZERO_SHIFT_COUNTS));
if (ok) ok = captureStage(1, static_cast<uint16_t>(original + ZERO_SHIFT_COUNTS));
// Always attempt restoration, even if an earlier operation failed.
bool restored = setZero(original);
if (restored && ok) ok = captureStage(2, original);
restored = restored && readZero() == original;
if (!restored) {
ready = false;
Serial.println(“RESTORE FAILED: power-cycle BOTH sensor and MCU before continuing.”);
} else Serial.println(“Original zero restored and verified. FW remains 5.”);
for (unsigned j=0; j<3; ++j) summary(j);
Serial.println(ok && restored ? “=== ZERO SHIFT TEST DONE ===” : “=== ZERO SHIFT TEST INCOMPLETE ===”);
// Discard commands typed during the experiment; a batch never auto-repeats.
while (Serial.available()) Serial.read();
Serial.println(“IDLE: s=stream, b=new experiment, d=export saved valid stages.”);
}

void exportData() {
streaming = false;
Serial.println(“experiment,stage,fw,zero_counts,sample,t_us,raw,angle_deg”);
char line[180];
for (unsigned j=0; j<3; ++j) {
const Capture& c = captures[j];
if (!c.valid) continue;
for (unsigned i=0; i<SAMPLE_COUNT; ++i) {
snprintf(line, sizeof(line), “%lu,%s,%u,%u,%u,%lu,%u,%.6f”,
(unsigned long)experiment, stageNames[j], unsigned(FILTER_FW), unsigned(c.zero), i+1,
(unsigned long)(c.timeUs[i]-c.timeUs[0]), unsigned(c.raw[i]), c.raw[i]*DEG_PER_COUNT);
Serial.println(line);
}
}
Serial.println(“=== EXPORT END ===”);
}

void setup() {
digitalWrite(ENCODER_CS, HIGH);
pinMode(ENCODER_CS, OUTPUT);
SPI.setSCK(D8); SPI.setRX(D9); SPI.setTX(D10); SPI.begin();
Serial.begin(115200);
uint32_t started = millis();
while (!Serial && millis()-started < 10000) delay(10);
delay(300);
Serial.println(“MA600 zero-shift experiment: s=stream, b=one test, d=export.”);
Serial.println(“Writes are volatile; no NVM store. FW explicitly set to 5.”);
if (readRegister(28) != 0) {
Serial.println(“ERROR: expected reg28=0. Power-cycle sensor and MCU.”); return;
}
expectedFilter = (readRegister(13) & 0xF0u) | FILTER_FW;
ready = writeRegister(13, expectedFilter);
delay(100);
}

void loop() {
if (Serial.available()) {
char cmd = Serial.read();
if (cmd==‘b’ || cmd==‘B’) runExperiment();
else if (cmd==‘d’ || cmd==‘D’) exportData();
else if (cmd==‘s’ || cmd==‘S’) streaming = true;
}
if (!ready) {
if (millis()-lastStream >= 2000) {
lastStream = millis();
Serial.println(“CONFIG ERROR: power-cycle sensor and MCU, then reopen monitor.”);
}
return;
}
if (streaming && millis()-lastStream >= 50) {
lastStream = millis();
uint16_t raw = frame(0);
char line[100];
snprintf(line, sizeof(line), “fw=%u raw=%5u angle=%10.6f deg”,
unsigned(FILTER_FW), unsigned(raw), raw*DEG_PER_COUNT);
Serial.println(line);
}
}

The std decreasing when zero is configured confirms this is not a mechanical error (if it was, the zero configuration would not matter), so we are left with only communication between MCU and sensor.

The provided code looks good, I ran it using an Arduino MKR ZERO and got the following results:

Stage Mean Angle [º] Standard deviation [º] Peak-to-peak [º]
Original before 186.690020 0.012216 0.071411
Zero shifted 180º 6.685944 0.012148 0.071411
Original restored 186.685307 0.012538 0.071411

As can be seen, I was not capable of reproducing the issue, so the issue should be on the MCU-sensor interface for your particular setup.

I will share my email address on a private message, please, send me the raw data of these experiments so I can further investigate this issue.

We went to private messaging for a little more debugging, but I’ll update this public post with the answer, for future users.

The problem is the power supply on the RP2350, at least as designed on the Seeed XIAO board. When I change the 3V3 power supply for the sensor over to another source – a benchtop supply, for instance – the noise in the half-range goes away entirely. So I can use the RP2350 with the MA600 as long as I supply 3V3 from somewhere else. Instead of that, of course, I will just use a different MCU that doesn’t have this problem.

ChatGPT identifies that the Seeed XIAO RP2350 uses a switching power regulator SGM6029C, whereas the Seeed XIAO SAMD21 (that I’ve switched to, and works) uses a linear regulator XC6206P332. Furthermore, the switching power regulator’s PWM can use “intermittent pulses” at low load, making it sound much less smooth than it’s supposed to be. And that doesn’t seem to play nice with the MA600. I don’t know who is to blame, but the combination is bad. I have no idea how this causes noise only in half the angle range. But I don’t care when I can just move on to something that works.

Here are some possibly-related problems from other Seeed users (not using MPS products at all):

https://forum.seeedstudio.com/t/ra4m1-huge-noise-in-adc-values/281591

https://forum.seeedstudio.com/t/if-you-are-fighting-with-noise-from-the-buck-converter-xiao-ra4m1-there-is-a-solution/284306

It seems like the MA600 and MA600A are both providing good error levels, and continue to fit my tight form factor. Just be careful in choosing the microcontroller board.

Thanks for the forum support!

Thank you very much for circling back and sharing the resolution!

I’m very glad that the MA600A datasheet has a power supply requirement which says these are the conditions the power supply has to meet so that you can achieve the performance in the data sheet.

Voltage supply induced (7) (8) B = 45mT, TA = 25°C 0.1 0.2 deg/V

Well done MPS!

On your old and new board, you can measure the power supply noise and compare it to this spec and know what to expect.