Dual Brushless Motor ESC, 10-62V, up to 50A per motor. Motors ganged or independent, multiple control input methods, cycle-by-cycle current limit, speed mode and torque mode control. Motors tiny to kW. Speed limit and other parameters easily set in firmware. As used in 'The Brushless Brutalist' locomotive - www.jons-workshop.com. See also Model Engineer magazine June-October 2019.
Dependencies: mbed BufferedSerial Servo PCT2075 FastPWM
Update 17th August 2020 Radio control inputs completed
brushless_motor.cpp
- Committer:
- JonFreeman
- Date:
- 2020-08-16
- Revision:
- 17:cc9b854295d6
- Parent:
- 16:d1e4b9ad3b8b
File content as of revision 17:cc9b854295d6:
/* STM3_ESC Electronic Speed Controller board, drives Two Brushless Motors, full Four Quadrant Control. Jon Freeman B. Eng Hons 2015 - 2020 */ #include "mbed.h" //#include "users/mbed_official/code/mbed-dev/file/707f6e361f3e/targets/TARGET_STM/TARGET_STM32F4/TARGET_STM32F401xE/device/stm32f401xe.h" //#include "stm32f401xe.h" //#include "stm32f411xe.h" #include "STM3_ESC.h" #include "BufferedSerial.h" #include "brushless_motor.h" extern eeprom_settings user_settings ; //extern BufferedSerial pc; // The two com ports used. There is also an unused com port, com3 setup @ 1200 baud /** brushless_motor::brushless_motor (PinName iadc, PinName pwv, PinName pwi, const uint16_t * lutptr, PinName px, PinName py, PinName pz, PortName pn, int port_bit_mask, uint32_t rnum) : Motor_I(iadc), maxV(pwv,PWM_PRESECALER_DEFAULT), maxI(pwi,PWM_PRESECALER_DEFAULT), H1(px), H2(py), H3(pz), OP(pn, port_bit_mask) // Constructor */ brushless_motor::brushless_motor (PinName iadc, PinName pwv, PinName pwi, const uint16_t * lutptr, PinName px, PinName py, PinName pz, PortName pn, const uint16_t port_bit_mask, const uint32_t rnum) : Motor_I(iadc), maxV(pwv,PWM_PRESECALER_DEFAULT), maxI(pwi,PWM_PRESECALER_DEFAULT), H1(px), H2(py), H3(pz), OP(pn, port_bit_mask) // Constructor { // Constructor OP = 0; H1.mode (PullUp); // PullUp resistors enabled on all Hall sensor input pins H2.mode (PullUp); H3.mode (PullUp); H1.rise (callback(this, &brushless_motor::Hall_change)); // Attach handler to the rising interruptIn edge H1.fall (callback(this, &brushless_motor::Hall_change)); // Attach handler to the falling interruptIn edge H2.rise (callback(this, &brushless_motor::Hall_change)); // Attach handler to the rising interruptIn edge H2.fall (callback(this, &brushless_motor::Hall_change)); // Attach handler to the falling interruptIn edge H3.rise (callback(this, &brushless_motor::Hall_change)); // Attach handler to the rising interruptIn edge H3.fall (callback(this, &brushless_motor::Hall_change)); // Attach handler to the falling interruptIn edge Hall_total = 0; // mode can be only 0, 8, 16 or 24, lut row select for Handbrake, Forward, Reverse, or Regen Braking Hall_previous = 0; maxV.period_ticks (MAX_PWM_TICKS + 1); // around 18 kHz maxI.period_ticks (MAX_PWM_TICKS + 1); maxV.pulsewidth_ticks (MAX_PWM_TICKS - 20); // Motor voltage pwm is inverted, see MCP1630 data maxI.pulsewidth_ticks (MAX_PWM_TICKS / 30); // Motor current pwm is not inverted. Initial values for scope hanging test visible_mode = MOTOR_REGENBRAKE; inner_mode = MOTOR_REGENBRAKE; lut = lutptr; // Pointer to motor energisation bit pattern table current_sense_rs_offset = rnum; // This is position in user_settings.rd(current_sense_rs_offset) Hall_index[0] = Hall_index[1] = read_Halls (); tickleon = 0; direction = 0; angle_cnt = 0; // Incremented or decremented on each Hall event according to actual measured direction of travel encoder_error_cnt = 0; // Incremented when Hall transition not recognised as either direction last_V = last_I = 0.0; Idbl = 0.0; // numof_current_sense_rs = 1.0; RPM_filter = 0.0; dRPM = 0.0; V_clamp = 1.0 ; // Used to limit top speed motor_poles = 8; // Default to 8 pole motor } /** * void brushless_motor::sniff_current () { // Initiate ADC current reading of approx motor average current * This to be called in response to ticker timebase interrupt. * As designed, called at 200 micro second intervals (Feb 2019) * Updates double I.dbl current measured in milliamps * Reading not used elsewhere in this code but made available through command line for external controller */ void brushless_motor::sniff_current () { // Initiate ADC current reading const double sampweight = 0.01 ; /// (double)CURRENT_SAMPLES_AVERAGED ; const double shrinkby = 1.0 - sampweight; uint16_t samp = Motor_I.read_u16 (); // CHECK but thought to be called once per 200us for each motor - CORRECT Feb 2019 // double dbls = ((double)samp) * numof_current_sense_rs / 6.0; // reading in mA // double dbls = (((double)samp) / 6.0) * (double)user_settings.rd(current_sense_rs_offset); // reading in mA // double dbls = sampweight * (double)(samp * user_settings.rd(current_sense_rs_offset)) / 6.0; // reading in mA Idbl *= shrinkby; // Jan 2019 New recursive low pass filter // Idbl += dbls * sampweight; // Current reading available, however not certain this is of any real use Idbl += sampweight * (double)(samp * user_settings.rd(current_sense_rs_offset)) / 6.0; // Current reading available, however not certain this is of any real use } bool brushless_motor::poles (uint32_t p) { if (p == 4 || p == 6 || p == 8) { motor_poles = p; return true; } return false; } void brushless_motor::Hall_change () { int32_t delta_theta; const int32_t delta_theta_lut[] = { // Looks up -1 for forward move detected, +1 for reverse move detected, 0 for error or unknown 0, 0, 0, 0, 0, 0, 0, 0, // Previous Hindex was 0 0, 0, 0,-1, 0, 1, 0, 0, // Previous Hindex was 1 0, 0, 0, 1, 0, 0,-1, 0, // Previous Hindex was 2 0, 1,-1, 0, 0, 0, 0, 0, // Previous Hindex was 3 0, 0, 0, 0, 0,-1, 1, 0, // Previous Hindex was 4 0,-1, 0, 0, 1, 0, 0, 0, // Previous Hindex was 5 0, 0, 1, 0,-1, 0, 0, 0, // Previous Hindex was 6 0, 0, 0, 0, 0, 0, 0, 0, // Previous Hindex was 7 } ; Hall_index[0] = read_Halls (); delta_theta = delta_theta_lut[(Hall_index[1] << 3) | Hall_index[0]]; if (delta_theta == 0) // Should only ever be +1 in 1 direction, -1 in other direction. 0 indicates invalid Hall sensor transition detected. encoder_error_cnt++; // Never used Dec 2019 else angle_cnt += delta_theta; OP = lut[inner_mode | Hall_index[0]]; // changed mode to inner_mode 27/04/18 Hall_total++; Hall_index[1] = Hall_index[0]; } /** * void brushless_motor::set_direction (int dir) { * Used to set direction according to user_settings data from eeprom */ void brushless_motor::set_direction (uint32_t dir) { direction = (dir != 0) ? MOTOR_FORWARD | MOTOR_REVERSE : 0; // bits used in eor, FORWARD = 1<<3, REVERSE = 1<<4 } //uint32_t get_direction () { // return direction; //} uint32_t brushless_motor::read_Halls () { uint32_t x = 0; if (H1) x |= 1; if (H2) x |= 2; if (H3) x |= 4; return x; } void brushless_motor::high_side_off () // Jan 2019 Only ever called from main when high side gate drive charge might need pumping up { uint16_t p = OP; p &= lut[32]; // KEEP_L_MASK_A or B OP = p; } /* void brushless_motor::low_side_on () { maxV.pulsewidth_ticks (1); OP = lut[31]; // KEEP_L_MASK_A or B } */ extern int WatchDog; #define DRIVING (visible_mode == MOTOR_FORWARD || visible_mode == MOTOR_REVERSE) #define ESTOP (WatchDog == 0 && DRIVING) /** * void brushless_motor::set_V_limit (double p) // Sets max motor voltage. * * Set motor voltage limit from zero (p=0.0) to max link voltage (p=1.0) */ void brushless_motor::motor_voltage_refresh () // May alter motor voltage to reflect changes in V_clamp { double p = last_V; if ((V_clamp < last_V) && DRIVING) // Jan 2019 limit top speed when driving p = V_clamp; // If motor runnable, set voltage limit to lower of last_V and V_clamp p *= 0.95; // need limit, ffi see MCP1630 data p = 1.0 - p; // because pwm is wrong way up maxV.pulsewidth_ticks ((int)(p * MAX_PWM_TICKS)); // PWM output to MCP1630 inverted motor pwm as MCP1630 inverts } void brushless_motor::set_V_limit (double p) // Sets max motor voltage. { if (p < 0.0 || ESTOP) p = 0.0; if (p > 1.0) p = 1.0; last_V = p; // Retains last voltage limit demanded by driver motor_voltage_refresh () ; } /**void brushless_motor::set_I_limit (double p) // Sets max motor current. pwm integrated to dc ref voltage level * * Set motor current limit from zero (p=0.0) to max determined by current sense resistors (p=1.0) * Value sent to pwm with RC integrator acting as AnalogOut. * pwm capable of 0.0 <= V_out <= 3.3. This feeds MCP1630 V_Ref, range 0 to 2.7v * Therefore (2.7/3.3) = 0.82 factor included. * Jan 2019 - As yet uncalibrated, so let's have a go at working it out! * Voltage ax current sense resistors amplified by op-amp with gain 5.7 (see EasyPC file 'BrushlessSTM3.sch', U6, R63, R64) * This then put through pot divider (10k with 4k7 to ground, gain 0.32) reducing overall gain to 1.8 (accurate enough) * This connects to MCP1630 CS (current sense) pin which works over the voltage range 0.0 to 0.9v * Therefore 0.5v across current sense resistor is sufficient to turn driver off. * 0.5v across 0.05 ohm gives 10A per current sense resistor fitted. * ** NOTE ** This is fast acting cycle by cycle current limit, the 10A figure is therefore peak T_on current. * * Current flows through current sense reaistor when one high side and one low side switch are on, expect a rising ramp due to motor inductance. * When either switch is off, inductor current continues to flow but not through current sense resistors, through a parasitic diode instead. * Thus T_on current is measured, T_off current is not measured * This means current reading should approximate to current taken from the supply. Motor average current may be considerably higher. * During REGEN_BRAKING, current flows the 'wrong' way through sense resistors and can not be measured. * * Board designed to have 1, 2, 3 or 4 0R05 current sense resistors per motor for 10A, 20A, 30A or 40A peak motor currents */ //const double MPR = (double)((MAX_PWM_TICKS * 9) / 11); // Scales 3.3v pwm DAC output to 2.7v V_Ref input const uint32_t MPR = ((MAX_PWM_TICKS * 9) / 11); // Scales 3.3v pwm DAC output to 2.7v V_Ref input void brushless_motor::set_I_limit (double p) // Sets max motor current. pwm integrated to dc ref voltage level { if (p < 0.0 || ESTOP) p = 0.0; if (p > 1.0) p = 1.0; last_I = p; // Retains last current limit demanded by driver if (DRIVING) { p *= current_scale; } maxI.pulsewidth_ticks ((uint32_t)(p * MPR)); // PWM } void brushless_motor::I_scale (double p) // Sets max motor current. pwm integrated to dc ref voltage level { current_scale = p; if (DRIVING) { maxI.pulsewidth_ticks ((uint32_t)(last_I * p * MPR)); // PWM } } /** * void brushless_motor::speed_monitor_and_control () // ** CALL THIS 32 TIMES PER SECOND ** * Call this once per 'MAIN_LOOP_REPEAT_TIME_US= 31250' main loop pass to keep RPM and MPH figures correct * Tracks total transitions on Hall sensor inputs to determine speed. * Sets variables double dRPM of motor RPM, and double dMPH miles per hour * * Speed control - double target_speed as reference input. * * ** This is where any speed limit gets applied ** * Motor voltage reduced when at or over speed. Does NOT apply any braking * Scope for further improvement of control algorithm - crude implementation of PID with no I */ void brushless_motor::speed_monitor_and_control () // call this once per 'MAIN_LOOP_REPEAT_TIME_US= 31250' main loop pass to keep count = edges per sec { // // const double samp_scale = 0.275; // Tweak this value only to tune filter const double samp_scale = 0.6; // Increased May 2020 to improve ssl speed settling time const double shrink_by = 1.0 - samp_scale; double rpm, speed_error; uint32_t Hall_tot_copy = Hall_total; // Copy value for use throughout function as may get changed at any instant during exec ! moving_flag = true; if (Hall_previous == Hall_tot_copy) { // Test for motor turning or not moving_flag = false; // Zero Hall transitions since previous call - motor not moving tickleon = TICKLE_TIMES; // May need to tickle some charge into high side switch bootstrap supplies } rpm = (double) (((Hall_tot_copy - Hall_previous) * 640) / motor_poles); // (Motor Hall sensor transitions in previous 31250us) * 640 RPM_filter *= shrink_by; // rpm now true but lumpy 'RPM' during most recent 31250us corrected for number of motor poles RPM_filter += (rpm * samp_scale); // filtered RPM dRPM = RPM_filter; dMPH = user_settings.rpm2mph(RPM_filter); // Completed updates of RPM and MPH Hall_previous = Hall_tot_copy; speed_error = (dMPH - user_settings.top_speed()); // 'P' Proportional contribution to PID control bool clamp_change = false; if (speed_error > 0.0 && (inner_mode == MOTOR_FORWARD || inner_mode == MOTOR_REVERSE)) { // Speed control only makes sense when motor runnable V_clamp *= (1.0 - (speed_error / 60.0)); // Driving too fast, so lower clamp voltage a tiny bit clamp_change = true; } else { // Not going too fast, and maybe driving or not if (V_clamp < 0.99) { V_clamp += 0.015; if (V_clamp > 1.0) V_clamp = 1.0; clamp_change = true; } } if (clamp_change) { motor_voltage_refresh () ; } /* d = V_clamp + (speed_error * sdbl[2]) + ((dv_by_dt / 1000.0) * sdbl[4]); // Apply P+I+D (with I=0) control if (d > 1.0) d = 1.0; if (d < 0.0) d = 0.0; V_clamp = d; if (V_clamp < last_V) // Jan 2019 limit top speed when driving { d *= 0.95; // need limit, ffi see MCP1630 data d = 1.0 - d; // because pwm is wrong way up //FUCKETYFUCK maxV.pulsewidth_ticks ((int)(d * MAX_PWM_TICKS)); // PWM output to MCP1630 inverted motor pwm as MCP1630 inverts maxV.pulsewidth_ticks ((int)(d * MAX_PWM_TICKS)); // PWM output to MCP1630 inverted motor pwm as MCP1630 inverts } }*/ /* temp_tick++; if (temp_tick > 50) { // one and a bit second temp_tick = 0; pc.printf ("top speed %.1f, mph %.1f, speed_err %.1f, V_clamp%.3f\r\n", user_settings.top_speed(), dMPH, speed_error, V_clamp); } */ } bool brushless_motor::exists () { return ((Hall_index[0] > 0) && (Hall_index[0] < 7)) ; } bool brushless_motor::is_moving () { return moving_flag; } /** bool brushless_motor::set_mode (int m) Use to set motor to one mode of HANDBRAKE, FORWARD, REVERSE. Used to also be used to set REGENBRAKE, replaced by brake(x) function. If this causes change of mode, also sets V and I to zero. */ bool brushless_motor::set_mode (uint32_t m) { if ((m != MOTOR_HANDBRAKE) && (m != MOTOR_FORWARD) && (m != MOTOR_REVERSE) && (m != MOTOR_REGENBRAKE)) { // pc.printf ("Error in set_mode, invalid mode %d\r\n", m); return false; } if (visible_mode != m) { // Mode change, kill volts and amps to be safe set_V_limit (0.0); set_I_limit (0.0); visible_mode = m; } if (m == MOTOR_FORWARD || m == MOTOR_REVERSE) // 8 or 16 - these are effectively address bits of motor pattern lut m ^= direction; // direction set to 0 or (MOTOR_FORWARD | MOTOR_REVERSE), so has zero or two bits set inner_mode = m; // idea is to use inner_mode only in lut addressing, keep 'visible_mode' true regardless of setup data in eeprom return true; } void brushless_motor::brake (double brake_effort) { // pc.printf ("In motor::brake, user_settings.brake_effectiveness = %3f\r\n", user_settings.brake_effectiveness()); set_mode (MOTOR_REGENBRAKE); if (brake_effort > 1.0) brake_effort = 1.0; if (brake_effort < 0.0) brake_effort = 0.0; brake_effort *= user_settings.brake_effectiveness(); // set upper limit, this is essential - May2020 fixed, was 100 times too big brake_effort = sqrt (brake_effort); // to linearise effect set_V_limit (brake_effort) ; set_I_limit (1.0); V_clamp = 1.0; } uint32_t brushless_motor::get_mode () { return visible_mode; } void brushless_motor::motor_set () // Energise Port with data determined by Hall sensors { Hall_index[0] = read_Halls (); OP = lut[inner_mode | Hall_index[0]]; }