BLDC clicking at narrow pulse width

Hi,
Me and my father have during the last couple of months been constructing a BLDC ESC. We have gotten it to work and it spins the motor with around the same efficiency as a commercial ESC however, at lower pulse widths, the motor gives off a sporadic clicking. It averages once every one or two seconds and we can't seem to figure out what causes it. It depends solely on PWM as it comes at the same pulse width at different RPM when testing it with different input voltages. We have a small offset for the BEMF comparators, I don't know if that could cause an issue. I have searched google to infinity and beyond without any luck so what I hope to find here is someone who has experienced this in the past or has an idea of what could cause this issue. There is not a mechanical issue with the motor as it runs silently with a commercial ESC. Code for commutation is found below, hope someone has experience with these types of issues!
Best regards and merry Christmas
Martin

Code-disclaimer. I am aware that I am setting the WGM bits every commutation. This is because it somehow changes settings when esc is running. I have absolutely no idea why. I feel as if I have looked in every corner of the code and I can't seem to find where that would happen. I am using an ATmega 328 SMD chip and the Arduino IDE to upload! I also want to direct credit to ELECTRONOOBS whose code has inspired this personal project. I realize some of the comments are in Swedish and I apologize.

Posting code in the allowed 5-minute intervals

BLDC:

/* bldc.h
Header intended to contain all functions that keep the motor running
*/

#include <defines.h>


void bldc_move();
void set_bemf(int phase, int type);
void motor_start(void);
void beep(int time, int freq);
void reset_motor(void);
void AH_BL(void);
void AH_CL(void);
void BH_CL(void);
void BH_AL(void);
void CH_AL(void);
void CH_BL(void);

// All functions in the header follow below and are separated by lines like the one below this one: 
////////////////////////////////////////////////////////////////

void bldc_move() // Controlling which bridge leg of MOSFETs get the regulated PWM.
{
  //noInterrupts();
  switch(bldc_step)   // OCR0B = phase A, OCR1B = phase C, OCR1A = phase = B.
  {
    case 0:
    AH_BL();
    set_bemf(C, FALLING);
    break;
    case 1:
    AH_CL();
    set_bemf(B, RISING);
    break;
    case 2:
    BH_CL();
    set_bemf(A, FALLING);
    break;
    case 3:
    BH_AL();
    set_bemf(C, RISING);
    break;
    case 4:
    CH_AL();
    set_bemf(B, FALLING);
    break;
    case 5:
    CH_BL();
    set_bemf(A, RISING);
  }
  //interrupts();
}

////////////////////////////////////////////////////////////////

void AH_BL() // TCCR1A = OCR1n, TCCR0A = OCR0n.
{ 
  TCCR1A = ~((1 << COM1A1) | (1 << COM1B1)); 
  TCCR0A |= (1 << COM0B1); 
  TRANSPORTREG = 0b00010000;
}

////////////////////////////////////////////////////////////////

void AH_CL()
{
  TCCR1A &= ~((1 << COM1A1) | (1 << COM1B1));
  TCCR0A |= (1 << COM0B1); 
  TRANSPORTREG = 0b00001000; // 0x08
}

////////////////////////////////////////////////////////////////

void BH_CL()
{
  CLEAR_BIT(TCCR1A, WGM11);
  CLEAR_BIT(TCCR1B, WGM13);
  TCCR1A |= (1 << COM1A1);
  TCCR1A &= ~((1 << COM1B1));
  TCCR0A &= ~((1 << COM0B1)); 
  TRANSPORTREG = 0b00000100; // 0x04
}

////////////////////////////////////////////////////////////////

void BH_AL()
{
  CLEAR_BIT(TCCR1A, WGM11);
  CLEAR_BIT(TCCR1B, WGM13);
  TCCR1A |= (1 << COM1A1);
  TCCR1A &= ~((1 << COM1B1));
  TCCR0A &= ~((1 << COM0B1)); 
  TRANSPORTREG = 0b00010000;
}

////////////////////////////////////////////////////////////////

void CH_AL()
{
  CLEAR_BIT(TCCR1A, WGM11);
  CLEAR_BIT(TCCR1B, WGM13);
  TCCR1A &= ~((1 << COM1A1));
  TCCR1A |= (1 << COM1B1);
  TCCR0A &= ~((1 << COM0B1)); 
  TRANSPORTREG = 0b00001000; // 0x08
}

////////////////////////////////////////////////////////////////

void CH_BL()
{
  CLEAR_BIT(TCCR1A, WGM11);
  CLEAR_BIT(TCCR1B, WGM13);
  TCCR1A &= ~((1 << COM1A1));
  TCCR1A |= (1 << COM1B1);
  TCCR0A &= ~((1 << COM0B1));
  TRANSPORTREG = 0b00000100; // 0x04
}

////////////////////////////////////////////////////////////////

void ALL_OFF()
{
  TCCR1A &= ~((1 << COM1A1) | (1 << COM1B1));
  TCCR0A &= ~((1 << COM0B1)); 
  TRANSPORTREG = 0b00011100; // 0x1C
}

////////////////////////////////////////////////////////////////

void set_bemf(int phase, int type)
{
  switch (phase)
  {
    case A:
      if (type == RISING)
      {
        PCMSK1 = 0x04;    // Interrupt at pin 0b00000100 which corresponds to pin PC2. 
        pin_state = 0x04; // Desired value for the pin to have changed to stored in this variable.
      }
     else
     {
      PCMSK1 = 0x04;    // Interrupt at pin 0b00000100 which corresponds to pin PC2. 
      pin_state = 0;
     }
    break;
    case B:
      if (type == RISING)
      {
      PCMSK1 = 0x08;    // Interrupt at pin 0b00000100 which corresponds to pin PC3. 
      pin_state = 0x08;
      }
      else
      {
        PCMSK1 = 0x08;    // Interrupt at pin 0b00000100 which corresponds to pin PC3. 
      pin_state = 0;
      }
    break;
    case C:
      if (type == RISING)
      {
        PCMSK1 = 0x10;    // Interrupt at pin 0b00000100 which corresponds to pin PC4. 
      pin_state = 0x10;
      }
      else
      {
        PCMSK1 = 0x10;    // Interrupt at pin 0b00000100 which corresponds to pin PC4. 
      pin_state = 0;
      }
      break;
}
}

////////////////////////////////////////////////////////////////
  void reset_motor()
  {
    STATUS.motorON = False;
    STATUS.emergency = False;
    ALL_OFF();
    bldc_step = AB;
    set_bemf(A, RISING);
  }

Set up:

// 328.h Contains a commented set - up of the ATMEGA 328P microcontroller.

void init_328p(void)
{
  init(); 
  STATUS.allflags = False; // All flags to zero


ADMUX  |= (1 << REFS0) | (1 << ADLAR);   // External reference and 8 - bit ADC
  ADCSRA = 0x87;                     // ADC-module turned on. 0b10000111

  // Initiating inhibit transistor control pins.
 
  CLEAR_BIT(DDRC, DDC2);
  CLEAR_BIT(DDRC, DDC3);
  CLEAR_BIT(DDRC, DDC4);

  CLEAR_BIT(PORTC, PORTC2);
  CLEAR_BIT(PORTC, PORTC3);
  CLEAR_BIT(PORTC, PORTC4); 

  CLEAR_BIT(DDRC, DDC1);
  SET_BIT(PORTC, PORTC1); // For break-checking

  DDRD = 0b00111100;
  SET_BIT(DDRB, DDB1);
  SET_BIT(DDRB, DDB2);
  CLEAR_BIT(PORTB,PORTB1);
  CLEAR_BIT(PORTB,PORTB2);
  PORTD = 0;  // Setting standard mode for PWM-pins to ground.
    // Setting three pins to PWM-Out. OC0B OC1B OC1A

  noInterrupts();

  // Using timer 2 for timekeeping

  TCCR0A |= (1 << COM0B1) | (1 << WGM01) | (1 << WGM00);   // Setting tyoe of PWM for OC0B.
  TCCR0B |= (1 << CS01); // (1 << WGM02);
  TCCR1B |= (1 << WGM12) | (1 << CS11);
  CLEAR_BIT(TCCR1B,CS10);
  CLEAR_BIT(TCCR1B,CS12);
  TCCR1A |= (1 << WGM10);
  TCCR1A |= (1 << COM1A1)|(1 << COM1B1);
  //TCCR1A = 0x01;   // Fixing PWM frequency.
  //TCCR1B = 0x09;
  TCCR0B &= ~((1 << CS00) | (1 << CS02));
  // Interrupts for BEMF sensing.

  PCICR = 0x02;      // PCICR = 0x03
  PCMSK1 = 0x04;      // Setting first intrerrupt at PCINT10. 0b00000100
  //PCMSK0 = 0x08;

  // Timekeeping

  TCCR2A = 0x02; // CTC mode
  SET_BIT(TCCR2B, CS21);  // Prescaling 8
  CLEAR_BIT(TCCR2B, CS22);
  CLEAR_BIT(TCCR2B, CS20);
  TIMSK2 = 0x02;// Enabling CTC with OCR2A
  TCNT2 = 0;
  OCR2A = 200;
  interrupts();
}

Main functions:

// Innehåller de stora funktionerna som löper kontinuerligt i main().

#include <defines.h>

void regulator(void);
void statemachine(void); 
void ADCupdate(void);
void esc_start(void);


/* regulator()

   Regulating pulse width in order to keep motor current
   to stay below the set current limit IMAX and to compensate
   for battery voltage dropping under high load.
   Input: None, Output: None
*/

void regulator(void)        
{
  if (!STATUS.test_mode)
  {
  pulse_width_reference = (ADCREADINGS.ACCELERATOR * maxduty) >> BYTE;   
  }
  if (!STATUS.regen)
  {
    if (ADCREADINGS.BAT <= BATREFERENCE) // Scaling up pulse_width if battery voltage has dropped.
    {
      SCALEFACTOR = (BATREFERENCE / ADCREADINGS.BAT);
    }
    else
    {
      SCALEFACTOR = 1;
    }
    // Regulator working with the squared error. This is a type of PI with some 
    // derivative aspect as its changes the change of regulation speed depening on 
    // the size of the error Lets call it a PId regulator instead of a PID. 
    if (REALREADINGS.SHUNT > IMAX)
    {
      errorterm += (REALREADINGS.SHUNT - IMAX) / ERRORCOEFF;
      pulse_width -= errorterm + SQUARED((REALREADINGS.SHUNT - IMAX)) / REGCOEFF;
    }
    else
    {
      if (pulse_width < pulse_width_reference * SCALEFACTOR)
      {
        errorterm =+ (pulse_width_reference * SCALEFACTOR - pulse_width) / ERRORCOEFF;
        if ((ADCREADINGS.SHUNT - IMAX < pulse_width_reference * SCALEFACTOR - pulse_width) && ADCREADINGS.SHUNT > 30)
        {
          pulse_width += errorterm + SQUARED((ADCREADINGS.SHUNT - IMAX)) / REGCOEFF;
        }
        else 
        {
          pulse_width += errorterm + SQUARED(pulse_width - pulse_width_reference * SCALEFACTOR) / REGCOEFF;
        }
      }
      else if (pulse_width > pulse_width_reference * SCALEFACTOR)
      {
        errorterm -= (pulse_width_reference * SCALEFACTOR - pulse_width) / ERRORCOEFF;
        pulse_width -= errorterm + SQUARED(pulse_width - pulse_width_reference * SCALEFACTOR) / REGCOEFF;
      }
    }
  }
  else if (STATUS.regen)  // Here is a modified regulation working in the oposite direction when we are slowing down.
  {                       // To keep current down when decelerating we want the battery voltage to match the induced coil voltage of the motor
    if (REALREADINGS.SHUNT > IREGMAX)
    {
      pulse_width += SQUARED((ADCREADINGS.SHUNT - IREGMAX)) / REGCOEFF;    // Current has priority
    }
    else if (REALREADINGS.SHUNT - IREGMAX < V_BEMF - V_EFF)       
    { 
      pulse_width -= SQUARED((ADCREADINGS.SHUNT - IREGMAX)) / REGCOEFF;
    }
    else
    {
      pulse_width -= SQUARED(V_BEMF - V_EFF) / (REGCOEFF << 1);
    }
  }

  // Clamp

if (pulse_width > maxpwm)
{
  pulse_width = maxpwm;
}
else if (pulse_width < 0)
{
  pulse_width = 0;
}

  if (ABS(REALREADINGS.SHUNT - IMAX) < 0.5 or ABS(pulse_width_reference * SCALEFACTOR - pulse_width) < 0.5)   // Setting error to zero at very small differences
  {
    errorterm = 0;
  }
  if (!STATUS.emergency) // Make sure emergency flag hasn't been set.
  {
    SET_PWM((int)pulse_width);
  }
}

/////////////////////////////////////////////////////

/*  statemachine()

    Making sure everything is 
    executed att the desired time intervals,
    setting flags and checking what the motorcycle is doing.
    Input: none, Output: none
*/

void statemachine(void)
{
  if (timer2cnt >= fastcompare)
  {
    STATUS.fastflag = True;
    slow_timer_cnt++;
    medium_timer_cnt++;
    timer2cnt = 0;
    phasetime++;
  }

  if (medium_timer_cnt >= mediumcompare)
  {                                                                   
    STATUS.breakON = check_pin(PINC,PINC1);
    RPM = 30*(pulse_width / 255)*KV; 
  }

  if (slow_timer_cnt >= slowcompare) // Code to be run once every second.
  {
    slow_timer_cnt = 0;  
  }

  if (STATUS.emergency)
  {
    ALL_OFF();
    phasetime = False;
    while (!phasetime)
    {
      phasetime = True;
      delay(1000); // 1s.
    }
    goto motor_stopped;
  }

  if (phasetime >= OFFTIME)          // Check if motor i still, in that case, prepare for a new start.
  {
    motor_stopped:
      phasetime = OFFTIME;
      reset_motor();
  }

  
  if (REALREADINGS.SHUNT < NO_LOAD && REALREADINGS.BAT < VMIN && 0)
  {
    ALL_OFF();
    while (True)
    {}
  }

}

/////////////////////////////////////////////////////

/* ADCupdate()

    Updating measured values used throughout the code.
    Input: none, Output: none.

*/

void ADCupdate(void)
{

  ADCREADINGS.BAT = read_adc(VBAT);
  ADCREADINGS.SHUNT = read_adc(SHUNT);
  ADCREADINGS.ACCELERATOR = read_adc(ACCELERATORPOT);


  // Transforming the adc - values to SI units. Future use for these would be to display on LCD. 

  REALREADINGS.SHUNT = ADCREADINGS.SHUNT; // Here the shunt has been dimensioned so the adc-step matches the current value.
  REALREADINGS.BAT = (ADCREADINGS.BAT * 50) >> 8;
  // Not neccesary to read as frequently

}

void esc_start(void)
{
  STATUS.allflags = False; // Setting all flags to zero
  STATUS.fastflag = True;  // Making sure we get measurements right away
  ALL_OFF();
  bldc_step = AB;
  set_bemf(A, RISING);

 // To do: Add measurement of battery voltage when current = 0 to get an idea of battery percentage.
}

What PWM scheme are you using?

I am using a square wave scheme, according to the picture I attached if that is what you mean. However, we have designed the circuit so that the low side MOSFETs have an inverted pulse with a set dead time while in the PWM - sequence so that the phases are switched between the battery voltage and GND instead of between battery voltage and floating.

media-1179126-bldc2fig4.jpg

media-1179126-bldc2fig4.jpg

Access to a multi-channel oscilloscope?

Sure do! :slight_smile: