Electronics > Projects, Designs, and Technical Stuff

DC motor speed controller with regen braking, using ATTINY85/45.

(1/11) > >>

Refrigerator:
Decided to post my ongoing project here. It's not a big project but a fun one, i think.

I have this homemade e-scooter that has gone through various changes and upgrades.
Honestly it's a pile of junk (quite literally) but fun to play around with (especially for my younger brother).

I had made a simple PWM controller for the motor with an LM393, which worked pretty well. Until i popped it  :-BROKE
And before that i had just an on off switch that would either give power to the motor or short it out to brake.
I really liked the electric brake but there was no easy way to add it to the LM393 PWM controller so i decided to make a new one.

This one will be controlled by an Attiny85/45. I'm currently testing the code on a Digispark board and Aruino IDE because it's more convenient.
Once it's all done i'll flash the code to a spare Attiny45 using my USBAsp. But right now the code is still a bit of a mess.

Currently in my code i have:
*Throttle end point edjustment
*Brake end point adjustment (same pot as throttle)
*Dead band adjustment between throttle and brake.
*Throttle max PWM adjustment
*Brake max PWM adjustment
*Limp mode PWM adjustment (when battery is low)
*Battery voltage low treshold
*Battery voltage cutoff treshold
*Battery voltage sag compensation (because voltage sags under load)
*Battery voltage divider ratio (for the resistor divider)
*Runaway protection (for when you connect the battery)

Have yet to add to the code:
*Current measurement
*Current limit
*Hysteresis for state changes (cutoff <-> limp <-> full power)

Tell me if i missed some important feature.

The regen will be done quite crudely with a MOSFET in parallel to the freewheeling diode and a diode in parallel to the drive MOSFET.
Basically a boost converter in parallel to the driver.

I wanted to do the current measurement without a shunt by basically measuring the VDS-ON of the drive MOSFET but my PWM frequency is at 33kHz so my ┬Ácontroller would be too slow i think. 33kHz because i wanted no PWM whine.
So i think i'll just use a galvanized nail as a shunt (galvanized because they solder easier).
When stalled the motor draws about 60-ish amps, so i need a pretty good shunt, hence the nail.
And no i'm not buying a shunt, that would take too long and cost too much.


--- Code: ---/* throttle and brake end point adjustment
works pretty much the same as on an RC car
  |<------------------------- ADC value from throttle potentiometer ---------------------->|
  |<------- throttle range --------->|<- dead band ->|<------ brake range for regen ------>|
  |                                  |               |                                     |
  |                                  |               |                                     |
  |                                  |               |                                     |
throttle_max_val           throttle_min_val       brake_min_val                      brake_max_val

*/

#define ADC_reference_voltage 2.56  // volts


/*~~~~ Start of user parameters ~~~~*/
// throttle values
  #define throttle_max_val    200 // expected throttle value max
  #define throttle_min_val    100 // expected throttle value min

//Brake values
  #define brake_min_val    80 // expected brake value min
  #define brake_max_val    50 // expected brake value max

// power limits
  uint8_t motor_PWM_max = 200;  // max power limit to motor from 0 to 255
  uint8_t motor_PWM_limp = 100; // max pwm when in limp mode once battery voltage is too low, cannot be above max PWM
  uint8_t brake_PWM_max = 255;  // max pwm to brake from 0 to 255
                        //note at 255 PWM brake there will be no regen
// battery parameters
  #define battery_voltage_low         13.6  // 3.4V per cell on a 4S pack   
  #define battery_voltage_cutoff      12.5  // 3.125V per cell
  #define battery_voltage_sag         10    // % of battery voltage sag at 100% throttle
  #define battery_voltage_divider_ratio   10  // battery voltage is divided for ADC by x value   
/*~~~~ end of user parameters ~~~~*/


// other stuff
#define throttle_range (throttle_max_val - throttle_min_val) // throttle range calculated
#define brake_range   (brake_min_val - brake_max_val) // min and max reversed fromthrottle because math
//                   ^^^    brackets are for math reasons


// variables
uint8_t throttle_val;   // throttle value variable for adc readout
uint16_t battery_val;    // battery value variable for adc readout
uint8_t PWM_to_throttle_register;  // calculated PWM value to send to register
uint8_t PWM_to_brake_register;     // calculated PWM value to send to register
uint16_t battery_voltage_ADC_range ; //
//uint8_t runaway_protection_flag; // self explanatory (and also not needed)

void setup() {

   
 
    GTCCR = 0b100000000; // timer setup register 1 = timer halted
    TCCR0B = 0b00000001;  // set clock source for timer
    TCCR0A = 0b10100001; // enable OC0A1 and set waveform gen to slow pwm
   
    DDRB = 0b00000011;  // set PB0 as output.

    OCR0A = 0;
    OCR0B = 0;

  ADMUX =
            (1 << ADLAR) |     // ADC works as 8-bit
            (1 << REFS1) |     // Set vref to 2.56V
            (1 << REFS2) |     // ^^^^^^^^^^^^^^^^^^
            (0 << MUX3)  |     //
            (0 << MUX2)  |     //
            (0 << MUX1)  |     //
            (1 << MUX0);       // ADC 1 on PB2

  ADCSRA =
            (1 << ADEN)  |     // Enable ADC
            (1 << ADPS2) |     // set prescaler to 64, bit 2
            (1 << ADPS1) |     // set prescaler to 64, bit 1
            (0 << ADPS0);      // set prescaler to 64, bit 0 


    // if throttle is not in the deadband wait until it is
    while (ADCH > throttle_min_val || ADCH < brake_min_val){
       ADCSRA |= (1 << ADSC);         // start ADC measurement
      while (ADCSRA & (1 << ADSC) ); // wait til conversion complete
    }
    // this should prevent runaway to some extent
   
}

void loop() {


  // measure throttle ADC
    // set analog multiplexer
  ADMUX &= ~( 1 << MUX1 ); // ADC 1 on PB2
 
    // measure voltage
  ADCSRA |= (1 << ADSC);         // start ADC measurement
    while (ADCSRA & (1 << ADSC) ); // wait til conversion complete
    throttle_val = ADCH;  // read adc val

//~~~~~~~~~~~~~~~~~~~~~~
  if (throttle_val > throttle_min_val){ // if throttle is applied
    PWM_to_brake_register = 0;  // brake PWM = 0
    if (throttle_val < throttle_max_val){
    PWM_to_throttle_register = ((uint16_t)(throttle_val - throttle_min_val)* motor_PWM_max) / throttle_range;  // scale value
    }
    else PWM_to_throttle_register = motor_PWM_max;
  }
  //~~~~~~~~~~~~~~~~~~~~~~~
   else if (throttle_val < brake_min_val){    // if brake is applied
    PWM_to_throttle_register = 0;  // throttle PWM = 0
    if (throttle_val > brake_max_val){
    PWM_to_brake_register = ((uint16_t)(brake_min_val - throttle_val) * brake_PWM_max) / brake_range ;  // scale value
    }
    else PWM_to_brake_register = brake_PWM_max;
  }
  else {
    PWM_to_brake_register = 0;
    PWM_to_throttle_register = 0;
  }

//*     // <- to comment out battery voltage measurement
  // measure battery voltage
    // set analog multiplexer
  ADMUX |= ( 1 << MUX1 ); // ADC 3 on PB3

    // measure the voltage
            ADCSRA |= (1 << ADSC);         // start ADC measurement
    while (ADCSRA & (1 << ADSC) ); // wait til conversion complete
    //battery_val = (ADCH << 2) | (ADCL >> 6);  // read adc val   
    battery_val = (uint16_t)(ADCL >> 6) | (uint16_t)(ADCH << 2);  // read adc val
   
    //adjust battery value according to load PWM
    battery_val += (((battery_val * battery_voltage_sag) / 100) * PWM_to_throttle_register ) / motor_PWM_max ;
    if (battery_val > 1023) battery_val = 1023;
//*/

  // transfer values to PWM registers
  OCR0A = PWM_to_brake_register; 
  //OCR0B = PWM_to_throttle_register;
   
//*
  // control throttle according to battery voltage
  if ( battery_val <= (uint32_t)(1023 * battery_voltage_cutoff) / (ADC_reference_voltage*battery_voltage_divider_ratio)){
    OCR0B = 0; // if battery too low no throttle
  }
  else {
    if ( battery_val <= (uint32_t)(1023 * battery_voltage_low) / (ADC_reference_voltage*battery_voltage_divider_ratio)){
    if (PWM_to_throttle_register >  motor_PWM_limp) OCR0B = motor_PWM_limp; // if battery too low no throttle
    }
    else OCR0B = PWM_to_throttle_register;
  }
 
//  */
}

--- End code ---


Ps: now thinking about it i could totally build a driver with all these features without any microntrocontroller. Might be a project for another time.

Siwastaja:
Regen comes automatically by just using half-bridge with two controllable switches, i.e., two MOSFETs instead of the simplest MOSFET+diode. As discussed in the other thread in the beginner section, diodes in parallel with MOSFETs are not usually needed.

A gate driver capable of driving that high-side MOSFET is needed, a bootstrap IC is the cheapest and simplest solution and quite OK at that power level.

Always keeping either of the MOSFETs on, i.e., switching between low and high and adjusting the duty cycle controls between drive / brake force. If you want to coast, you can carefully feed back the duty cycle so that current measures zero, or simply just disable both MOSFETs.

And yes, the circuit is inherently equivalent to a synchronous buck, which is also equivalent to synchronous boost! The motor is equivalent to the L and C of the buck/boost, L being the winding inductance and C the inertia, voltage over C being the back-EMF voltage (voltage generated by the motor, linearly dependent on the RPM).

When you do add the current sense (and I'd recommend: start here!), keep in mind it needs to be bidirectional. There are bidirectional current sense amplifiers which have a bias pin so that zero current equals some output voltage (such as Vcc/2).

Siwastaja:
... and use a ground side shunt (i.e., from lower FET source to ground) instead of Vds sensing because it's easier to implement. I like your idea about a nail. You can calibrate it in software.

An amplifier would be highly recommendable but maybe the nail can dissipate enough power to cause measurable voltage difference with ADC only. Try to come up with some kind of ghetto "Kelvin connection" though. Biasing for bidirectional sensing would require some trickery though.

Refrigerator:

--- Quote from: Siwastaja on October 26, 2021, 06:19:02 pm ---Regen comes automatically by just using half-bridge with two controllable switches, i.e., two MOSFETs instead of the simplest MOSFET+diode. As discussed in the other thread in the beginner section, diodes in parallel with MOSFETs are not usually needed.

A gate driver capable of driving that high-side MOSFET is needed, a bootstrap IC is the cheapest and simplest solution and quite OK at that power level.

Always keeping either of the MOSFETs on, i.e., switching between low and high and adjusting the duty cycle controls between drive / brake force. If you want to coast, you can carefully feed back the duty cycle so that current measures zero, or simply just disable both MOSFETs.

And yes, the circuit is inherently equivalent to a synchronous buck, which is also equivalent to synchronous boost! The motor is equivalent to the L and C of the buck/boost, L being the winding inductance and C the inertia, voltage over C being the back-EMF voltage (voltage generated by the motor, linearly dependent on the RPM).

When you do add the current sense (and I'd recommend: start here!), keep in mind it needs to be bidirectional. There are bidirectional current sense amplifiers which have a bias pin so that zero current equals some output voltage (such as Vcc/2).

--- End quote ---

I considered the half bridge idea at first but was a bit afraid of accidentally turning on both MOSFETs at once, which would be pretty bad.
IIRC TCC1 in Attiny85/45 can generate two phase correct PWM signals with deadtime so i might look more into that.

As for the current measurement, i think i'll measure the low side current with a diff amp.
My Vref right now is the internal 2.56V, which i chose for stability.
But i might switch to the (potentially noisy) V+ as Vref. This way i can use V+/2 as a reference for a virtual ground for the diff amp.
Then at startup i would read the current at 0 PWM and assume that this value is my zero point.
Then i would only need to know the volt per amp ratio.
With a 5V reference my ADC would have approx 5mV resolution and about 4V of usable range, before i get into any non-linearities in the ADC and the diff amp.
And if i had, say 40mV/A, i could get about 0.125A resolution and a +-50A range.
I could use a simple 7805 to regulate my V+ and voltage stability wouldn't even matter because if V+ drifts so does the virtual ground along with it.

Looks like i might be whipping up a homemade PCB for this driver because this might be a little too complicated to build neatly on a perf-board.  :-/O

Benta:
Somehow, the approach here seems to be backwards to me, but I'm probably just old fashioned.

If I were to do a design like this, I'd start with the load: OK, predifined, it's a bike. Then I'd move on to the motor and it's specifications.
This would in turn lead me on to the power stage needed for this motor. When this is specified, gate drivers defined, switching frequency for good regulation selected, power management and, and, and, THEN I'd look at which MCU might be suitable.

Starting with "...using ATTINY85/45" is totally foreign to me. Sorry.

Navigation

[0] Message Index

[#] Next page

There was an error while thanking
Thanking...
Go to full version