Magnetic Encoder (4761) not working with Arduino

Good day everyone,

I am facing a weird problem with my Pololu magnetic encoder.

The problem is that I am currently trying to read it on my Arduino UNO R4 Minima; however, the output stays stuck at 1 and does not want to toggle into the usual quadrature format.

Here is the code I am using:

// Quadrature (Pololu 4761) on A=D2, B=D3 → RPM + speed (m/s) + distance (m) + ESC ramp
// Serial Plotter columns: A, B, rpmQuad, speed_mps, dist_m, ESCuS

#include <Arduino.h>
#include <Servo.h>

#ifndef IRAM_ATTR
#define IRAM_ATTR
#endif

// -------- Pins --------
const uint8_t pinA   = 2;    // External interrupt pin
const uint8_t pinB   = 3;    // External interrupt pin
// -------- Your empirical calibration --------
const float COUNTS_PER_METER = 615.0f;          // <-- your measured value
const float METERS_PER_COUNT = 1.0f / COUNTS_PER_METER;

// (Optional) keep RPM too. If unknown CPR, this is just "effective RPM" vs. your earlier assumption.
// If your encoder really is 12 counts/rev at 4x on the motor shaft and you're on the wheel, CPR_QUAD should be "wheel counts per wheel rev".
// You can ignore RPM if you only care about speed & distance.
const int CPR_QUAD = 12;  // keep as-is if you want RPM; otherwise you can delete all rpm code


// -------- Quadrature state (ISR) --------
const int8_t qtab[16] = {
  0, -1, +1,  0,
  +1, 0,  0, -1,
  -1, 0,  0, +1,
   0, +1, -1,  0
};

volatile long     q_count = 0;           // signed tick count
volatile uint8_t  q_prev  = 0;           // previous AB state
volatile uint32_t q_lastEdge_us = 0;     // for minimal deadtime
const uint16_t    q_dead_us = 10;        // ignore transitions arriving <10us apart

// -------- Kinematics computation (loop) --------
float rpmQuad = 0.0f;                    // signed RPM (optional)
float speed_mps = 0.0f;                  // signed linear speed (m/s)
float distance_m = 0.0f;                 // signed odometer (m)

const float rpmAlpha   = 0.9f;           // EMA smoothing for RPM (responsive)
const float speedAlpha = 0.5f;           // EMA smoothing for speed (moderate)

uint32_t  sampLast_us  = 0;
long      sampLast_cnt = 0;
const uint32_t SAMPLE_US = 25000;        // 25 ms window (~40 Hz updates)

// -------- Plot timing --------
const uint32_t PLOT_HZ = 100;
uint32_t lastPlot_us = 0;

// ==================== ISRs ====================
inline void quadStepFromAB(uint8_t A, uint8_t B, uint32_t now) {
  if ((now - q_lastEdge_us) < q_dead_us) return;  // deadtime

  uint8_t curr = (A << 1) | B;
  uint8_t idx  = (q_prev << 2) | curr;
  int8_t step  = qtab[idx];
  if (step) {
    q_count += step;
    q_lastEdge_us = now;
  }
  q_prev = curr; // keep state updated
}

void IRAM_ATTR isrA() {
  uint32_t now = micros();
  uint8_t A = digitalRead(pinA);
  uint8_t B = digitalRead(pinB);
  quadStepFromAB(A, B, now);
}

void IRAM_ATTR isrB() {
  uint32_t now = micros();
  uint8_t A = digitalRead(pinA);
  uint8_t B = digitalRead(pinB);
  quadStepFromAB(A, B, now);
}

// ==================== Helpers ====================

// Compute RPM (optional), speed (m/s), and distance (m) from delta counts over SAMPLE_US
void updateKinematics() {
  uint32_t now = micros();
  if ((now - sampLast_us) >= SAMPLE_US) {
    noInterrupts();
    long cnt = q_count;
    interrupts();

    long dcnt = cnt - sampLast_cnt;        // signed counts in window
    uint32_t dt_us = now - sampLast_us;    // us
    float dt_s = dt_us * 1e-6f;

    // ---- speed (m/s) from your counts-per-meter ----
    float inst_speed = (dcnt * METERS_PER_COUNT) / dt_s;  // signed m/s
    speed_mps = speedAlpha * inst_speed + (1.0f - speedAlpha) * speed_mps;

    // ---- distance (m) integrate counts ----
    distance_m += dcnt * METERS_PER_COUNT; // signed odometer

    // ---- RPM (optional) if CPR_QUAD is set meaningfully ----
    // RPM = (counts / CPR) * (60 / dt)
    float inst_rpm = (float)dcnt * 60.0f / (float(CPR_QUAD) / dt_s); // same as 60e6 / (CPR * dt_us) * dcnt
    rpmQuad = rpmAlpha * inst_rpm + (1.0f - rpmAlpha) * rpmQuad;

    // advance window
    sampLast_cnt = cnt;
    sampLast_us  = now;
  }
}

void plotNow() {
  const uint32_t now = micros();
  const uint32_t interval = 1000000UL / PLOT_HZ;
  if (now - lastPlot_us >= interval) {
    lastPlot_us = now;
    int A = digitalRead(pinA);
    int B = digitalRead(pinB);
    Serial.print(A);              Serial.print('\t');
    Serial.print(B);              Serial.print('\t');
    Serial.print(rpmQuad,   1);   Serial.print('\t');   // optional
    Serial.print(speed_mps, 3);   Serial.print('\t');
    
    // Changed to println() and removed the trailing tab
    Serial.println(distance_m, 3); 
  }
}

void rampUpAndDown() {
    updateKinematics();
    plotNow();
    if (Serial.available() && (Serial.read() == 'z')){Serial.print("Serial Problem");}
}

// ==================== Setup / Loop ====================
void setup() {
  Serial.begin(115200);

  pinMode(pinA, INPUT);  // Pololu board has pull-ups to its VCC
  pinMode(pinB, INPUT);

  // Initialize previous state
  q_prev = (digitalRead(pinA) << 1) | digitalRead(pinB);

  // Attach CHANGE interrupts on both channels
  attachInterrupt(digitalPinToInterrupt(pinA), isrA, CHANGE);
  attachInterrupt(digitalPinToInterrupt(pinB), isrB, CHANGE);

  // Initialize sampling
  sampLast_us  = micros();
  sampLast_cnt = 0;

  // Serial Plotter header
  Serial.println("A\tB\trpmQuad\tspeed_mps\tdist_m\tESCuS");
}

void loop() {
  rampUpAndDown();
}

And a quick image of my setup:

What’s weird is that this used to work before hand, but right now it is refusing to change.

It’s hard to tell from your picture what the encoder board is mounted to and measuring (i.e. where the encoder disc is mounted). Could you post some more pictures and details about that? Also, could you try measuring the voltage A and B output pins on the encoder board directly, either with an oscilloscope while the motor is running or with a multimeter while slowly turning the motor by hand?

Brandon

Good morning Brandon,
Apologizes for the late reply. Here are the photos that you requested:


The larger magnet’s slot is passed through the shaft of the motor and the disk is allowed to rotate.
Here is how I carried out the test placement for the sensor and magnet:

The results were a constant 3.338 volt at OUTA and OUTB with no variation as the magnet is slowly rotated or even quickly rotated beside them.

Kindly denote that I have tried to also flip the magnet and test out if there are any voltage variation as it spins, but the results didn’t budge.

It sounds like the encoder isn’t detecting the magnet at all. The intended alignment is for the magnet to be concentric with the center hole on the encoder board, and sit roughly 2mm away from the Hall effect sensors on the board, as shown here:


Could you try getting as close to that as possible and seeing if that changes anything?

Also, those encoder boards are sold in pairs; have you tried different variations of the encoder boards and magnets to see if the problem can be narrowed down to a specific board/magnet?

Brandon

I have tried using different magnets and encoders, and tried my best to carry the experiment as close as I can, but that yielded no differences.