Vector drive of 3-phase induction motor.

I've recently been experimenting with an old washing machine
motor of the 3-phase induction variety. Its nominally 195V but
I'm limited to 30V with current bench supply and inverter board
ATM (which makes things a lot safer of course).

The vector control technique for induction motors relies on
keeping separate control of the rotor flux vector's amplitude
and rate of rotation (slip speed).

This allows instant change of torque and operation as a servo
motor, since the slip speed and stator currents are controlled
in a coordinated fashion.

The basic circuit is a 3-phase inverter bridge (in my case 6 MOSFETs,
a FAN7388 3-phase driver chip and two hall-effect current sensors
to measure the stator current.

(And lots of cermaic decoupling capacitors of course).

The two tiny devices at top left and top right are little SPI ADC chips
capable of clocking to 20MHz and 1MSPS - these digitize the current
values from the ASC711's in a few microseconds, as the main control
loop has to be very fast.

The basic current control compares the three phase current values against
a reference set. The direction of the difference is determined from the
largest absolute value of the 3 phase errors, and this directly chooses
the next time-steps invert switching state. This loop runs at about 35kHz,
and direct port manipulation is key to getting this performance. Many
variables are only one byte to keep things lean and fast too.

Also the reference values are computed at each loop, using the
flux and torque input amplitudes combined with current rotor
position and slip-position. A cosine-lookup table is used to keep
this efficient and the entries are also 8 bit.

At the outer level a PID loop (currently only P and D terms) drives
the torque input and slip frequency (which is integrated by the inner
loop to get the slip phase). The outer loop also reads an absolute
magnetic encoder (AS5040) and runs at about 4kHz.

The end result is:

  • A fully functional servo motor, in theory scalable to 200V and 800W,
    motor was £20 on ebay (had to fiddle a bit to stop the rotor rubbing
    the stator, it had obviously been dropped).

The motor's tachogenerator was ripped out and an AS5040 magnetic encoder
fitting in its place, which has a 12 absolute angular resolution and update rate
of 96us.

And the current, very rough, code:

#define ENC_CS   7
#define ENC_SCLK 6
#define ENC_DATA 8

#define ADC_CSU  9
#define ADC_CSW  10


//#define USE_HI_COUNTS 1
//#define SATURATING_ADC 1

void setup ()
{
  pinMode (ADC_CSU, OUTPUT) ;
  pinMode (ADC_CSW, OUTPUT) ;
  pinMode (SCK, OUTPUT) ;
  pinMode (MOSI, OUTPUT) ;
  pinMode (ENC_CS, OUTPUT) ;
  pinMode (ENC_SCLK, OUTPUT) ; 

  for (byte i = A0 ; i <= A5 ; i++)
    pinMode (i, OUTPUT) ;
  PORTC = 0b010101 ;

  Serial.begin (57600) ;
  SPSR = 0x01 ;
  SPCR = 0x50 ;
  int junk =  SPSR ;
  junk &= SPDR ;
  crud (junk) ;
}

void crud (int junk)
{
  current_setup () ;
  calc_setup () ;
}


char adc ()
{
  SPDR = 0x00 ;
  while ((SPSR & (1<<SPIF)) == 0) {}
  byte hi = SPDR - 0x08 ;
  
  SPDR = 0x00 ;
  while ((SPSR & (1<<SPIF)) == 0) {}
  byte lo = SPDR ;
  PORTB |= 0x04 ;
  
  int res = (hi << 8) | lo ;
#ifdef SATURATING_ADC
  if (res > 508)
    return 127 ;
  if (res < -508)
    return -127 ;
#endif
  return (char) (res >> 2) ;  // not saturating arith yet.
}

// 166mV / amp at 5V,  136 count / amp
// want +/- 127 to be current range, actually represents +/- 508, so
// shift 12 bit value right 2, in other words 8 bits from 10 bits value.

char u_zero ;
char w_zero ;

char u_target ;
char w_target ;

void current_setup ()
{
  int sum = 0 ;
  char res ;
  for (byte i = 0 ; i < 64 ; i++)
  {
    PORTB &= 0xFD ;  // pin 9 CS
    res = adc () ;
    PORTB |= 0x02 ;
    sum += res ;
  }
  u_zero = (char) ((sum + 32) >> 6) ;
  sum = 0 ;
  for (byte i = 0 ; i < 64 ; i++)
  {
    PORTB &= 0xFB ;  // pin 10 CS
    res = adc () ;
    PORTB |= 0x04 ;
    sum += res ;
  }
  w_zero = (char) ((sum + 32) >> 6) ;

  u_target = 34 ;
  w_target = -17 ;
}


byte old_patt = 0 ;

#ifdef USE_HI_COUNTS
byte uhi_count = 0 ;
byte vhi_count = 0 ;
byte whi_count = 0 ;
#endif


byte cycle = 0 ;

int rotor_phase = 0 ;
long rotor_pos = 0L ;
int rotor_prev  = 0 ;
int rotor_freq  = 0 ;


void encoder ()   // upto 64 LSB changes per sample at 16000 rpm
{
  PORTD &= 0x7F ;  // ENC_CS
  int res = 0 ;
  for (byte i = 0 ; i < 16 ; i++)
  {
    PORTD &= 0xBF ;  // ENC_SCLK
    res |= PINB & 1 ;
    PORTD |= 0x40 ;  // ENC_SCLK
    res <<= 1 ;
   
  }
  res |= PINB & 1 ;
  res &= 0xFFC0 ;  // ignore status
  ;
  PORTD |= 0x80 ;  // ENC_CS
  rotor_phase = res ; // upto 0x1000 change per sample so expand to use more bits
  rotor_freq += (((rotor_phase - rotor_prev) << 3) - rotor_freq + 8) >> 4 ;  // digital filter, average over 4ms or so
  rotor_pos += (rotor_phase - rotor_prev) >> 6 ;
  rotor_prev = rotor_phase ;
}
  
void current_loop ()
{
  cycle += 1 ;
 
  PORTB &= 0xFD ;  // pin 9 CS
  char u_actual = adc () - u_zero ;  // truncated value
  PORTB |= 0x02 ;
  
  PORTB &= 0xFB ;
  char w_actual = w_zero - adc () ;
  PORTB |= 0x04 ;

  char u_error = u_target - u_actual ;
  char w_error = w_target - w_actual ;
  char v_error = - u_error - w_error ;

  char u_abs = u_error < 0 ? -u_error : u_error ;
  char v_abs = v_error < 0 ? -v_error : v_error ;
  char w_abs = w_error < 0 ? -w_error : w_error ;

  char max_abs = u_abs > v_abs ?
    (w_abs > u_abs ? w_abs : u_abs) :
    (w_abs > v_abs ? w_abs : v_abs) ;

  byte pattern = 0b010101 ;

  if (max_abs > 2)  // threshold value, a guess currently
  {
    if (u_abs >= v_abs && u_abs > w_abs)
    {
      if (u_error >= 0)
      {
	pattern = 0b100101 ;
#ifdef USE_HI_COUNTS
	vhi_count = 0 ;
	whi_count = 0 ;
#endif
      }
      else
      {
	pattern = 0b011010 ;
#ifdef USE_HI_COUNTS
	uhi_count = 0 ;
#endif
      }
    }
    else if (v_abs >= w_abs && v_abs > u_abs)
    {
      if (v_error >= 0)
      {
	pattern = 0b011001 ;
#ifdef USE_HI_COUNTS
	uhi_count = 0 ;
	whi_count = 0 ;
#endif
      }
      else
      {
	pattern = 0b100110 ;
#ifdef USE_HI_COUNTS
	vhi_count = 0 ;
#endif
      }
    }
    else if (w_abs >= u_abs && w_abs > v_abs)
    {
      if (w_error >= 0)
      {
	pattern = 0b010110 ;
#ifdef USE_HI_COUNTS
	uhi_count = 0 ;
	vhi_count = 0 ;
#endif
      }
      else
      {
	pattern = 0b101001 ;
#ifdef USE_HI_COUNTS
	whi_count = 0 ;
#endif
      }
    }
  }
#ifdef USE_HI_COUNTS
  if (pattern == 0b010101)
  {
    uhi_count = 0 ;
    vhi_count = 0 ;
    whi_count = 0 ;
  }
 
  if (uhi_count == 100)
  {
    pattern ^= 0b110000 ;
    uhi_count = 0 ;
  }
  if (vhi_count == 100)
  {
    pattern ^= 0b001100 ;
    vhi_count = 0 ;
  }
  if (whi_count == 100)
  {
    pattern ^= 0b000011 ;
    whi_count = 0 ;
  }
#else
  if ((cycle & 0x3F) == 0)
    pattern = 0b010101 ;
#endif

  byte diff = pattern ^ old_patt ;
  old_patt = pattern ;
  
  PORTC &= ~diff ;
#ifdef USE_HI_COUNTS
  uhi_count ++ ;
  vhi_count ++ ;
  whi_count ++ ;
#else
  asm ("nop") ;
  asm ("nop") ;
  asm ("nop") ;
#endif
  PORTC = pattern ;
}


int  flux_coeff = 34 * 141 / 100 ; // units of ADC, 1A = 34
int  torque_coeff = 300/5 ;



char cos_tab [0x100] ;

void calc_setup ()
{
  for (int i = 0 ; i < 0x100 ; i++)
  {
    float angle = PI * i / 128.0 ;
    float cosine = cos (angle) ;
    cos_tab [i] = (char) (127.0 * cosine) ;
  }
}

long slip_phase = 0L ;
long slip_freq = 300L << 11 ;

void calc_loop (boolean do_u)
{
  slip_phase += slip_freq ;
  long angle = rotor_phase ;
  angle = (angle << 16) + slip_phase ;
  byte ph = ((byte *) &angle)[3] ;
  if (do_u)
  {
    byte ph2 = ph + 0x40 ;
    int u = cos_tab [ph]  * flux_coeff + cos_tab [ph2] * torque_coeff ;
    u_target = (char) ((u+64) >> 7) ;
  }
  else
  {
     byte ph3 = ph - 0x55 ;
     byte ph4 = ph - 0x15 ;
     int w = cos_tab [ph3] * flux_coeff + cos_tab [ph4] * torque_coeff ;
     w_target = (char) ((w+64) >> 7) ;
  }
}
  

char read_u ()
{
  PORTB &= 0xFD ;  // pin 9 CS
  char res = adc () ;
  PORTB |= 0x02 ;
  return res ;
}

char read_w ()
{
  PORTB &= 0xFB ;  // pin 9 CS
  char res = adc () ;
  PORTB |= 0x04 ;
  return res ;
}

long target = 0L ;

void pid ()
{
  long error = (target - rotor_pos) << 3 ; // * 10 ;
  error -= rotor_freq ;
  if (error > 250) error = 250 ;
  if (error < -250) error = -250 ;
  slip_freq = error ;
  slip_freq <<= 10 ;
  torque_coeff = (error + (error << 2)) >> 4 ;
  flux_coeff = 48 ; // units of ADC, 1A = 34
}

unsigned long prev_ts = 0L ;
byte * ptr = 0 ;

void loop ()
{
  if ((cycle & 7) == 0)
  {
    slip_phase += slip_freq ;
    encoder () ;
  }
  if ((cycle & 15) == 2)
  {
    pid () ;
    
    if (millis () - prev_ts > 100)
    {
      prev_ts += 100 ;
      target = 2 * *ptr++ ;
    }
    
  }
  current_loop () ;
  calc_loop (false) ;
  current_loop () ;
  calc_loop (true) ;
}

You didn't mention anywhere..This is DTC and not FOC right? That's my guess. Please confirm. Also some more info would be nice. Like equations used. You said that there is a current control loop running at 35kHz that follows the reference currents. Please elaborate. I thought the Hysteresis control is applied to torque and flux and not the currents.

I'm just driving the current directly - I've got an encoder on the motor, so
no rotor estimation is needed I know where it is so all I do is set the
input torque / flux components and slip speed directly. The control loop
switches the bridge to keep the measured currents matching the desired ones.

The torque/flux phasors are basically rotated by a phasor that rotates at
actual speed + desired slip speed.

 int res = (hi << 8) | lo ;
#ifdef SATURATING_ADC
 if (res > 508)
   return 127 ;
 if (res < -508)
   return -127 ;
#endif

What happens if res is exactly equal to 508?

Here's a hint:

#ifdef SATURATING_ADC
  if (res > 508)
    return 127 ;
  if (res < -508)
    return -127 ;
#endif
  return (char) (res >> 2) ;  // not saturating arith yet.

You seem to have missed that -508 is not +508 and that 127*4 = 508,
in GPU terms this is just clamp (res/4, -127, 127)

@MarkT Have you got any data on the electronics you designed? I'm currently investigating position control of linear induction motors and am looking into a proper 3 phase current driver to use.

@tobias559 Have you founded any proper 3 phase current driver to use? If not, you may take a look at the Veichi AC70 vector control VFD drive.

MarkT:
You seem to have missed that -508 is not +508

Duh. My bad.

May i have the components list please. Thanks. Robin

 Serial.begin (57600) ;

--?--