Unwanted behaviour of ACS712 with Arduino Uno R4 Wifi

Hi everyone,

I'm working on controlling the torque of a geared DC motor, but I'm currently stuck on calibrating my current sensor.

My encoder is accurately evaluating both position and direction. However, the current sensor data is unreliable. I've tried several methods to enhance and filter the output, but nothing has worked so far.

To help provide context, I've attached the following files:

  • Schematic: (Please note: I have changed a few pins from the original design - PWM2 = 9, DIR2 = 8, CHA = 3, CHB = 2). and I have used a 10K ohm resistance and Capacitor 104 (0.1 microfarad) with the analog output of the sensor.

  • Calibration Test File: My isolated code for testing the current sensor.

  • Trial Code: This code tracks the rotor in a sin wave form (works perfectly, except for the current readings).

  • Plots: Visuals obtained from the serial data.

Has anyone experienced similar issues or have any recommendations on standard practices to properly calibrate this sensor? Any advice or insights would be greatly appreciated.

Thanks in advance for your time and help!

Current Sensor Caliberation : (Ran this for about a 1.30 minutes)

#include <Arduino.h>
#include "CytronMotorDriver.h"
#include "ACS712.h"
#include <pwm.h>

PwmOut pwmPin(9);
CytronMD motor(PWM_DIR, 9, 8);
ACS712 acs(A0, 5.0, 8192, 185);

#define CPR              1428L   
#define DEFAULT_SPEED    160
#define OVERSAMPLE_N     64
#define OVERSAMPLE_US    25 //32 -> 25
#define FILTER_ALPHA     0.25f
#define PRINT_EVERY_MS   50UL


volatile long    encoderCounts = 0;
volatile uint8_t prevState     = 0;

static const int8_t ENC_TABLE[16] = {
   0,  1, -1,  0,
  -1,  0,  0,  1,
   1,  0,  0, -1,
   0, -1,  1,  0
};

void doEncoder() {
  uint8_t a = digitalRead(3);
  uint8_t b = digitalRead(2);
  uint8_t cur = (a << 1) | b;
  encoderCounts += ENC_TABLE[(prevState << 2) | cur];
  prevState = cur;
}
// ----------------------------------------

bool  running    = false;
float filteredI  = 0.0f;
int   currentSpd = DEFAULT_SPEED;

void driveMotor(int spd) { motor.setSpeed(spd); }

float readCurrentAmps() {
  long sum = 0;
  for (uint8_t i = 0; i < OVERSAMPLE_N; i++) {
    sum += acs.mA_DC();
    delayMicroseconds(OVERSAMPLE_US);
  }
  return (sum / (float)OVERSAMPLE_N) / 1000.0f;
}

void setup() {
  Serial.begin(115200);
  analogReadResolution(14);
  pwmPin.begin(10000.0f, 5.0f);

  pinMode(3, INPUT_PULLUP);
  pinMode(2, INPUT_PULLUP);
  uint8_t a = digitalRead(3);
  uint8_t b = digitalRead(2);
  prevState = (a << 1) | b;
  attachInterrupt(digitalPinToInterrupt(3), doEncoder, CHANGE);
  attachInterrupt(digitalPinToInterrupt(2), doEncoder, CHANGE);

  Serial.println(F("# Calibrating — keep motor still..."));
  acs.setMidPoint(8192);
  long mpSum = 0;
  for (uint8_t i = 0; i < 10; i++) {
    acs.autoMidPoint();
    mpSum += acs.getMidPoint();
    delay(100);
  }
  acs.setMidPoint((int)(mpSum / 10));

  Serial.print(F("# Midpoint = "));
  Serial.println(acs.getMidPoint());
  Serial.println(F("# Ready. Send START."));
}

void loop() {
  static uint32_t lastPrint   = 0;
  static long     lastCounts  = 0;
  static uint32_t lastRpmTime = 0;
  static float    rpm         = 0.0f;

  if (Serial.available()) {
    String cmd = Serial.readStringUntil('\n');
    cmd.trim();
    cmd.toUpperCase();

    if (cmd == "START") {
      if (!running) {
        running      = true;
        currentSpd   = DEFAULT_SPEED;
        noInterrupts(); encoderCounts = 0; interrupts();
        lastCounts   = 0;
        rpm          = 0.0f;
        driveMotor(currentSpd);
        filteredI    = readCurrentAmps();
        lastPrint    = millis();
        lastRpmTime  = millis();
        Serial.println(F("t_ms,spd_cmd,I_raw_A,I_filt_A,RPM"));
      }
    } else if (cmd == "STOP") {
      running = false;
      driveMotor(0);
      Serial.println(F("# Stopped."));
    } else if (running) {
      int val = cmd.toInt();
      if (val != 0 || cmd == "0") {
        currentSpd = constrain(val, -255, 255);
        driveMotor(currentSpd);
        Serial.print(F("# Speed -> ")); Serial.println(currentSpd);
      }
    }
  }

  if (!running) return;

  uint32_t now = millis();
  if (now - lastPrint >= PRINT_EVERY_MS) {
    uint32_t dt = now - lastRpmTime;

    noInterrupts();
    long cnt = encoderCounts;
    interrupts();

    if (dt > 0) {
      long deltaCounts = cnt - lastCounts;
      rpm = (deltaCounts / (float)CPR) * (60000.0f / dt);
      lastCounts  = cnt;
      lastRpmTime = now;
    }

    float iRaw = readCurrentAmps();
    filteredI += FILTER_ALPHA * (iRaw - filteredI);

    Serial.print(now);          Serial.print(',');
    Serial.print(currentSpd);   Serial.print(',');
    Serial.print(iRaw,  4);     Serial.print(',');
    Serial.print(filteredI, 4); Serial.print(',');
    Serial.println(rpm, 1);

    lastPrint = now;
  }
}

output :

Trail project : Sine wave Tracking

#include <Arduino.h>
#include "CytronMotorDriver.h"

#define CURRENT_PIN  A0
#define SENSITIVITY  0.066f //0.185 ->
#define CPR          1428L
#define PRINT_MS     200UL

CytronMD motor(PWM_DIR, 9, 8);
inline void driveMotor(int spd) { motor.setSpeed(-spd); }

volatile long    counts    = 0;
volatile uint8_t prevState = 0;
static const int8_t ENC_TABLE[16] = {
   0,  1, -1,  0,
  -1,  0,  0,  1,
   1,  0,  0, -1,
   0, -1,  1,  0
};
void doEncoder() {
  uint8_t a = digitalRead(3);
  uint8_t b = digitalRead(2);
  uint8_t cur = (a << 1) | b;
  counts += ENC_TABLE[(prevState << 2) | cur];
  prevState = cur;
}

float offsetV   = 0.0f;
float filteredI = 0.0f;
void updateCurrent() {
  float v = analogRead(CURRENT_PIN) * (5.0f / 16383.0f);
  filteredI += 0.10f * ((v - offsetV) / SENSITIVITY - filteredI); //low pass filter with alpha = 0.01
}

const float kp            = 6.0f;
const float kd            = 0.5f; // 1.5 -> 0.5
const float kff           = 0.0f; // 15 -> 0
const float amplitudeRads = 3.0f * PI;
const float periodSecs    = 20.0f;
const float omega         = TWO_PI / periodSecs;
const float N             = 21.0f;   // dirty-derivative cutoff (rad/s); tune 15–40 //15 -> 21

float lastError   = 0.0f;
float posFilt     = 0.0f;
float posFiltPrev = 0.0f;
bool  isRunning   = false;
unsigned long startTime = 0;
unsigned long lastTime  = 0;

void setup() {
  Serial.begin(115200);
  analogReadResolution(14);
  pinMode(3, INPUT_PULLUP);
  pinMode(2, INPUT_PULLUP);
  uint8_t a = digitalRead(3);
  uint8_t b = digitalRead(2);
  prevState = (a << 1) | b;
  attachInterrupt(digitalPinToInterrupt(3), doEncoder, CHANGE);
  attachInterrupt(digitalPinToInterrupt(2), doEncoder, CHANGE);

  Serial.println(F("Calibrating..."));
  long sum = 0;
  for (int i = 0; i < 500; i++) { sum += analogRead(CURRENT_PIN); delay(2); }
  offsetV = (sum / 500.0f) * (5.0f / 16383.0f);
  Serial.print(F("OffsetV=")); Serial.println(offsetV, 4);
  driveMotor(7);
  Serial.println(F("Send START"));
}

void loop() {
  static uint32_t lastPrint = 0;
  unsigned long now = millis();

  if (!isRunning) {
    if (Serial.available()) {
      String s = Serial.readStringUntil('\n');
      s.trim(); s.toUpperCase();
      if (s == F("START")) {
        isRunning    = true;
        startTime    = now;
        lastTime     = now;
        lastPrint    = now;
        lastError    = 0.0f;
        posFilt      = 0.0f;
        posFiltPrev  = 0.0f;
        noInterrupts(); counts = 0; interrupts();
        driveMotor(0);
        Serial.println(F("t_ms,target_rad,actual_rad,targetW,omega_meas,pwm,I_filt"));
      }
    }
    return;
  }

  if (now - lastTime >= 3UL) {
    float dt = (now - lastTime) * 0.001f;
    lastTime = now;

    noInterrupts();
    long cnt = counts;
    interrupts();

    updateCurrent();

    float actualRad = (cnt / (float)CPR) * TWO_PI;
    float t         = (now - startTime) * 0.001f;
    float targetRad = amplitudeRads * sinf(omega * t);
    float targetW   = amplitudeRads * omega * cosf(omega * t);
    float error     = targetRad - actualRad;
    float deriv     = (error - lastError) / dt;
    lastError       = error;

    float alpha   = N * dt / (1.0f + N * dt);
    posFilt      += alpha * (actualRad - posFilt);
    float omegaMeas = (posFilt - posFiltPrev) / dt;
    posFiltPrev   = posFilt;

    float vOut = kp * error + kd * deriv + kff * targetW;
    int pwm = (int)constrain(vOut / 12.0f * 255.0f, -255.0f, 255.0f);
    driveMotor(pwm);

    if (now - lastPrint >= PRINT_MS) {
      lastPrint = now;
      Serial.print(now - startTime); Serial.print(',');
      Serial.print(targetRad, 4);    Serial.print(',');
      Serial.print(actualRad, 4);    Serial.print(',');
      Serial.print(targetW, 4);      Serial.print(',');
      Serial.print(omegaMeas, 4);    Serial.print(',');
      Serial.print(pwm);             Serial.print(',');
      Serial.println(filteredI, 4);
    }

    if (t >= periodSecs * 2.0f) {
      driveMotor(0);
      Serial.println(F("DONE"));
      isRunning = false;
    }
  }
}

Output

Several questions pop up,

If the motor is not running does the ACS712 read a zero current?

Not exactly zero...the serial monitor displays some mAmps.

There is always some noise in the ADC, so not unexpected.

(not the cause of the problem)
I had a quick scan of your code and one minor point in readCurrentAmps().
sum should be a float as acs.mA_DC() returns a float. if sum is a long it will structurally be rounded down, OVERSAMPLE_N times.

float readCurrentAmps() {
  float sum = 0.0;
  for (uint8_t i = 0; i < OVERSAMPLE_N; i++) {
    sum += acs.mA_DC();
    delayMicroseconds(OVERSAMPLE_US);
  }
  return (sum / (float)OVERSAMPLE_N) / 1000.0f;
}

digging further.

Please try setting void suppressNoise(bool flag);, it averages 2 ADC samples per mA_DC() call.

float readCurrentAmps() {
  acs.suppressNoise(true);
  float sum = 0.0;
  for (uint8_t i = 0; i < OVERSAMPLE_N; i++) {
    sum += acs.mA_DC();
    delayMicroseconds(OVERSAMPLE_US);
  }
  return (sum / (float)OVERSAMPLE_N) / 1000.0f;
}

Does this improve the reading?

Is it correct that the motor is controlled by a PWM signal?

First of all Thanks for helping me out... yes the motor is controlled via pwm signal

After running for few seconds, I obtained the following :

That means that the ADC will measure (at least) 2 alternating values, say 0 and A.
64 samples are averaged, and depending how many 0's and A's the average can be anything between 0 and A.

Here a quick rewrite float readCurrentAmps() so it average measurements over an interval of time.

float readCurrentAmps(uint32_t duration)
{
  acs.suppressNoise(true);
  float sum = 0.0;
  uint32_t count = 0;
  uint32_t start = millis();
  while (millis() - start <= duration)
  {
     sum += acs.mA_DC();
     count++;
  }
  return (sum / count) * 0.001f ;  //  return Amps
}

I propose to start with a duration of 25 milliseconds, which should give you roughly 40 measurements per second.

That said, to measure the average current of a PWM signal one should follow another tactic. One has to measure a whole number of duty cycles and average that.
(I'll be back with some code later)

OK, it reduced the noise quite a bit, got the feeling we're heading in the right direction.

Is there any co-relation between my PWM frequency and the sampling rate of the current sensor data. Or is there any way that the Arduino ADC might bottleneck as the current sensor bandwidht is 80KHz ?

wrote a quick and dirty PWM current measurement, based upon one LOW HIGH cycle.
Can you give it a try?

float readCurrentAmpsPWM()
{
  float sum = 0.0;
  uint32_t count = 0;
  float value = 0;
  
  //  wait for LOW
  while (acs.mA_DC() < 50);  //  assumption zero noise is way below 50 mA
  //  wait for HIGH
  while (acs.mA_DC() > 50);

  //  while LOW we have effectively zero Amps, => not add to sum.
  while (acs.mA_DC() < 50)
  {
    count++;  //  count zero levels
  }
  //  while HIGH we must add to sum.
  while ((val = acs.mA_DC()) > 50)
  {
    sum += value;
    count++;
  }
  return (sum / count) * 0.001f ;  //  return Amps
}

The PWM frequency is relative low about 500 kHz = 2 ms per pulse by default.
An ADC sample is about 25 us,
So to capture a full LOW HIGH cycle one needs to make 2000 / 25 ~ 80 samples.
If you do more or less, you measure not a whole cycle
( OK with ~160 you would have 2 cycles)


If the readCurrentAmpsPWM() works I will add it to the ACS712 library.
Note: need to add a times = 1 parameter to get T whole cycles to improve accuracy.

Created - Add mA_DC_PWM() function · Issue #58 · RobTillaart/ACS712 · GitHub

@iotaisreal has the PWM frequency = 10kHz and has a 159Hz LPF at the ACS712 output. So the output of the filter should basically be a constant voltage with very little noise.

What is the current rating for your ACS712 module?
What is the typical current your motor will use?

based upon the 185 mVperA parameter it should be a 5 A device.
5 Volt in 8192 steps ~ 0.6mV per step.

Are you sure you have them connected right?
Are your solderless breadboard connections good?

ACS712 acs(A0, 5.0, 8192, 185);

analogReadResolution(14);

Never worked with the R4 or used an ACS712 library, but
these lines seem to use two different resolutions.
8192 points to 13-bit, and should be -1, so 8191 if the resolution is set to 13.

Or 16383 (16384 -1) if resolution stays at 14.

I had to guess you use the 5A version of the ACS712 (the185(mV) in the code).

Please explain how and why. It does not show in your connection diagram.

Firstly, as suggested 8192 is the midpoint of the ACS when analogRead in 14-bit. Second, due to some constraint I was unable to update my current schematics hence uploaded the old one (I apologize for that !)

As of now I am in very starting phase of my project so I am using a breadboard and I had tried placing my Hall Effect sensor away from power supply wires.