-
Notifications
You must be signed in to change notification settings - Fork 719
Expand file tree
/
Copy pathFOCMotor.cpp
More file actions
987 lines (857 loc) · 36.8 KB
/
Copy pathFOCMotor.cpp
File metadata and controls
987 lines (857 loc) · 36.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
#include "FOCMotor.h"
#include "../../communication/SimpleFOCDebug.h"
/**
* Default constructor - setting all variabels to default values
*/
FOCMotor::FOCMotor()
{
// maximum angular velocity to be used for positioning
velocity_limit = DEF_VEL_LIM;
// maximum voltage to be set to the motor
voltage_limit = DEF_POWER_SUPPLY;
// not set on the begining
current_limit = DEF_CURRENT_LIM;
// index search velocity
velocity_index_search = DEF_INDEX_SEARCH_TARGET_VELOCITY;
// sensor and motor align voltage
voltage_sensor_align = DEF_VOLTAGE_SENSOR_ALIGN;
// default modulation is SinePWM
foc_modulation = FOCModulationType::SinePWM;
// default target value
target = 0;
voltage.d = 0;
voltage.q = 0;
// current target values
current_sp = 0;
current.q = 0;
current.d = 0;
// voltage bemf
voltage_bemf = 0;
// Initialize phase voltages U alpha and U beta used for inverse Park and Clarke transform
Ualpha = 0;
Ubeta = 0;
//monitor_port
monitor_port = nullptr;
//sensor
sensor_offset = 0.0f;
sensor = nullptr;
//current sensor
current_sense = nullptr;
}
/**
Sensor linking method
*/
void FOCMotor::linkSensor(Sensor* _sensor) {
sensor = _sensor;
}
/**
CurrentSense linking method
*/
void FOCMotor::linkCurrentSense(CurrentSense* _current_sense) {
current_sense = _current_sense;
}
// shaft angle calculation
float FOCMotor::shaftAngle() {
// if no sensor linked return previous value ( for open loop )
if(!sensor) return shaft_angle;
return sensor_direction*sensor->getAngle() - sensor_offset;
}
// shaft velocity calculation
float FOCMotor::shaftVelocity() {
// if no sensor linked return previous value ( for open loop )
if(!sensor) return shaft_velocity;
return sensor_direction*LPF_velocity(sensor->getVelocity());
}
float FOCMotor::electricalAngle(){
// if no sensor linked return previous value ( for open loop )
if(!sensor) return electrical_angle;
return _normalizeAngle( (float)(sensor_direction * pole_pairs) * sensor->getMechanicalAngle()
#ifdef INTEGER_ANGLE
* _2PI / sensor->getStepsPerRevolution()
#endif
- zero_electric_angle );
}
/**
* Monitoring functions
*/
// function implementing the monitor_port setter
void FOCMotor::useMonitoring(Print &print){
monitor_port = &print; //operate on the address of print
#ifndef SIMPLEFOC_DISABLE_DEBUG
SimpleFOCDebug::enable(&print);
SIMPLEFOC_MOTOR_DEBUG("Monitor enabled!");
#endif
}
// Measure resistance and inductance of a motor
int FOCMotor::characteriseMotor(float voltage, float correction_factor=1.0f){
auto old_status = motor_status;
motor_status = FOCMotorStatus::motor_calibrating;
if (!this->current_sense || !this->current_sense->initialized)
{
SIMPLEFOC_MOTOR_ERROR("Fail. CS not init.");
motor_status = old_status;
return 1;
}
if (voltage <= 0.0f){
SIMPLEFOC_MOTOR_ERROR("Fail. Volt. <= 0");
motor_status = old_status;
return 2;
}
voltage = _constrain(voltage, 0.0f, voltage_limit);
SIMPLEFOC_MOTOR_DEBUG("Meas R..");
float current_electric_angle = electricalAngle();
// Apply zero volts and measure a zero reference
setPhaseVoltage(0, 0, current_electric_angle);
_delay(500);
PhaseCurrent_s zerocurrent_raw = current_sense->readAverageCurrents();
DQCurrent_s zerocurrent = current_sense->getDQCurrents(current_sense->getABCurrents(zerocurrent_raw), current_electric_angle);
// Ramp and hold the voltage to measure resistance
// 300 ms of ramping
current_electric_angle = electricalAngle();
for(int i=0; i < 100; i++){
setPhaseVoltage(0, voltage/100.0f*((float)i), current_electric_angle);
_delay(3);
}
_delay(10);
PhaseCurrent_s r_currents_raw = current_sense->readAverageCurrents();
DQCurrent_s r_currents = current_sense->getDQCurrents(current_sense->getABCurrents(r_currents_raw), current_electric_angle);
// Zero again
setPhaseVoltage(0, 0, current_electric_angle);
if (fabsf(r_currents.d - zerocurrent.d) < 0.2f)
{
SIMPLEFOC_MOTOR_ERROR("Fail. current too low");
motor_status = old_status;
return 3;
}
float resistance = voltage / (correction_factor * (r_currents.d - zerocurrent.d));
if (resistance <= 0.0f)
{
SIMPLEFOC_MOTOR_ERROR("Fail. Est. R<= 0");
motor_status = old_status;
return 4;
}
SIMPLEFOC_MOTOR_DEBUG("Est. R: ", 2.0f * resistance);
_delay(100);
// Start inductance measurement
SIMPLEFOC_MOTOR_DEBUG("Meas L...");
unsigned long t0 = 0;
unsigned long t1 = 0;
float Ltemp = 0;
float Ld = 0;
float Lq = 0;
float d_electrical_angle = 0;
unsigned int iterations = 40; // how often the algorithm gets repeated.
unsigned int cycles = 3; // averaged measurements for each iteration
unsigned int risetime_us = 200; // initially short for worst case scenario with low inductance
unsigned int settle_us = 100000; // initially long for worst case scenario with high inductance
// Pre-rotate the angle to the q-axis (only useful with sensor, else no harm in doing it)
current_electric_angle += 0.5f * _PI;
current_electric_angle = _normalizeAngle(current_electric_angle);
for (size_t axis = 0; axis < 2; axis++)
{
for (size_t i = 0; i < iterations; i++)
{
// current_electric_angle = i * _2PI / iterations; // <-- Do a sweep of the inductance. Use eg. for graphing
float inductanced = 0.0f;
float qcurrent = 0.0f;
float dcurrent = 0.0f;
for (size_t j = 0; j < cycles; j++)
{
// read zero current
zerocurrent_raw = current_sense->readAverageCurrents(20);
zerocurrent = current_sense->getDQCurrents(current_sense->getABCurrents(zerocurrent_raw), current_electric_angle);
// step the voltage
setPhaseVoltage(0, voltage, current_electric_angle);
t0 = micros();
delayMicroseconds(risetime_us); // wait a little bit
PhaseCurrent_s l_currents_raw = current_sense->getPhaseCurrents();
t1 = micros();
setPhaseVoltage(0, 0, current_electric_angle);
DQCurrent_s l_currents = current_sense->getDQCurrents(current_sense->getABCurrents(l_currents_raw), current_electric_angle);
delayMicroseconds(settle_us); // wait a bit for the currents to go to 0 again
if (t0 > t1) continue; // safety first
// calculate the inductance
float dt = (t1 - t0)/1000000.0f;
if (l_currents.d - zerocurrent.d <= 0 || (voltage - resistance * (l_currents.d - zerocurrent.d)) <= 0)
{
continue;
}
inductanced += fabsf(- (resistance * dt) / logf((voltage - resistance * (l_currents.d - zerocurrent.d)) / voltage))/correction_factor;
qcurrent+= l_currents.q - zerocurrent.q; // average the measured currents
dcurrent+= l_currents.d - zerocurrent.d;
}
qcurrent /= cycles;
dcurrent /= cycles;
float delta = qcurrent / (fabsf(dcurrent) + fabsf(qcurrent));
inductanced /= cycles;
Ltemp = i < 2 ? inductanced : Ltemp * 0.6f + inductanced * 0.4f;
float timeconstant = fabsf(Ltemp / resistance); // Timeconstant of an RL circuit (L/R)
// SIMPLEFOC_MOTOR_DEBUG("Estimated time constant in us: ", 1000000.0f * timeconstant);
// Wait as long as possible (due to limited timing accuracy & sample rate), but as short as needed (while the current still changes)
risetime_us = _constrain(risetime_us * 0.6f + 0.4f * 1000000 * 0.6f * timeconstant, 100, 10000);
settle_us = 10 * risetime_us;
// Serial.printf(">inductance:%f:%f|xy\n", current_electric_angle, Ltemp * 1000.0f); // <-- Plot an angle sweep
/**
* How this code works:
* If we apply a current spike in the d´-axis, there will be cross coupling to the q´-axis current, if we didn´t use the actual d-axis (ie. d´ != d).
* This has to do with saliency (Ld != Lq).
* The amount of cross coupled current is somewhat proportional to the angle error, which means that if we iteratively change the angle to min/maximise this current, we get the correct d-axis (and q-axis).
*/
if (axis)
{
qcurrent *= -1.0f; // to d or q axis
}
if (qcurrent < 0)
{
current_electric_angle+=fabsf(delta);
} else
{
current_electric_angle-=fabsf(delta);
}
current_electric_angle = _normalizeAngle(current_electric_angle);
// Average the d-axis angle further for calculating the electrical zero later
if (axis)
{
d_electrical_angle = i < 2 ? current_electric_angle : d_electrical_angle * 0.9f + current_electric_angle * 0.1f;
}
}
// We know the minimum is 0.5*PI radians away, so less iterations are needed.
current_electric_angle += 0.5f * _PI;
current_electric_angle = _normalizeAngle(current_electric_angle);
iterations /= 2;
if (axis == 0)
{
Lq = Ltemp;
}else
{
Ld = Ltemp;
}
}
if (sensor)
{
/**
* The d_electrical_angle should now be aligned to the d axis or the -d axis. We can therefore calculate two possible electrical zero angles.
* We then report the one closest to the actual value. This could be useful if the zero search method is not reliable enough (eg. high pole count).
*/
float estimated_zero_electric_angle_A = _normalizeAngle( (float)(sensor_direction * pole_pairs) * sensor->getMechanicalAngle()
#ifdef INTEGER_ANGLE
* _2PI / sensor->getStepsPerRevolution()
#endif
- d_electrical_angle);
float estimated_zero_electric_angle_B = _normalizeAngle( (float)(sensor_direction * pole_pairs) * sensor->getMechanicalAngle()
#ifdef INTEGER_ANGLE
* _2PI / sensor->getStepsPerRevolution()
#endif
- d_electrical_angle + _PI);
float estimated_zero_electric_angle = 0.0f;
if (fabsf(estimated_zero_electric_angle_A - zero_electric_angle) < fabsf(estimated_zero_electric_angle_B - zero_electric_angle))
{
estimated_zero_electric_angle = estimated_zero_electric_angle_A;
} else
{
estimated_zero_electric_angle = estimated_zero_electric_angle_B;
}
SIMPLEFOC_MOTOR_DEBUG("New el. zero: ", estimated_zero_electric_angle);
SIMPLEFOC_MOTOR_DEBUG("Curr. el. zero: ", zero_electric_angle);
}
SIMPLEFOC_MOTOR_DEBUG("Ld [mH]: ", Ld * 1000.0f);
SIMPLEFOC_MOTOR_DEBUG("Lq [mH]: ", Lq * 1000.0f);
if (Ld > Lq)
{
SIMPLEFOC_MOTOR_WARN("Ld>Lq. Likely error.");
}
if (Ld * 2.0f < Lq)
{
SIMPLEFOC_MOTOR_WARN("Lq > 2*Ld. Likely error.");
}
// store the measured values
phase_resistance = 2.0f * resistance;
axis_inductance = {Ld, Lq};
phase_inductance = (Ld + Lq) / 2.0f; // FOR BACKWARDS COMPATIBILITY
motor_status = old_status;
return 0;
}
// utility function intended to be used with serial plotter to monitor motor variables
// significantly slowing the execution down!!!!
void FOCMotor::monitor() {
if( !monitor_downsample || monitor_cnt++ < (monitor_downsample-1) ) return;
monitor_cnt = 0;
if(!monitor_port) return;
bool printed = 0;
if(monitor_variables & _MON_TARGET){
if(!printed && monitor_start_char) monitor_port->print(monitor_start_char);
monitor_port->print(target,monitor_decimals);
printed= true;
}
if(monitor_variables & _MON_VOLT_Q) {
if(!printed && monitor_start_char) monitor_port->print(monitor_start_char);
else if(printed) monitor_port->print(monitor_separator);
monitor_port->print(voltage.q,monitor_decimals);
printed= true;
}
if(monitor_variables & _MON_VOLT_D) {
if(!printed && monitor_start_char) monitor_port->print(monitor_start_char);
else if(printed) monitor_port->print(monitor_separator);
monitor_port->print(voltage.d,monitor_decimals);
printed= true;
}
// read currents if possible - even in voltage mode (if current_sense available)
if(monitor_variables & _MON_CURR_Q || monitor_variables & _MON_CURR_D) {
DQCurrent_s c = current;
if( current_sense && torque_controller != TorqueControlType::foc_current ){
c = current_sense->getFOCCurrents(electrical_angle);
c.q = LPF_current_q(c.q);
c.d = LPF_current_d(c.d);
}
if(monitor_variables & _MON_CURR_Q) {
if(!printed && monitor_start_char) monitor_port->print(monitor_start_char);
else if(printed) monitor_port->print(monitor_separator);
monitor_port->print(c.q*1000, monitor_decimals); // mAmps
printed= true;
}
if(monitor_variables & _MON_CURR_D) {
if(!printed && monitor_start_char) monitor_port->print(monitor_start_char);
else if(printed) monitor_port->print(monitor_separator);
monitor_port->print(c.d*1000, monitor_decimals); // mAmps
printed= true;
}
}
if(monitor_variables & _MON_VEL) {
if(!printed && monitor_start_char) monitor_port->print(monitor_start_char);
else if(printed) monitor_port->print(monitor_separator);
monitor_port->print(shaft_velocity,monitor_decimals);
printed= true;
}
if(monitor_variables & _MON_ANGLE) {
if(!printed && monitor_start_char) monitor_port->print(monitor_start_char);
else if(printed) monitor_port->print(monitor_separator);
monitor_port->print(shaft_angle,monitor_decimals);
printed= true;
}
if(printed){
if(monitor_end_char) monitor_port->println(monitor_end_char);
else monitor_port->println("");
}
}
// Function (iterative) generating open loop movement for target velocity
// - target_velocity - rad/s
// it uses voltage_limit variable
float FOCMotor::velocityOpenloop(float target_velocity){
// get current timestamp
unsigned long now_us = _micros();
// calculate the sample time from last call
float Ts = (now_us - open_loop_timestamp) * 1e-6f;
// quick fix for strange cases (micros overflow + timestamp not defined)
if(Ts <= 0 || Ts > 0.5f) Ts = 1e-3f;
// calculate the necessary angle to achieve target velocity
shaft_angle = _normalizeAngle(shaft_angle + target_velocity*Ts);
// for display purposes
shaft_velocity = target_velocity;
// save timestamp for next call
open_loop_timestamp = now_us;
if (torque_controller == TorqueControlType::voltage)
return voltage_limit;
else
return current_limit;
}
// Function (iterative) generating open loop movement towards the target angle
// - target_angle - rad
// it uses voltage_limit and velocity_limit variables
float FOCMotor::angleOpenloop(float target_angle){
// get current timestamp
unsigned long now_us = _micros();
// calculate the sample time from last call
float Ts = (now_us - open_loop_timestamp) * 1e-6f;
// quick fix for strange cases (micros overflow + timestamp not defined)
if(Ts <= 0 || Ts > 0.5f) Ts = 1e-3f;
// calculate the necessary angle to move from current position towards target angle
// with maximal velocity (velocity_limit)
// TODO sensor precision: this calculation is not numerically precise. The angle can grow to the point
// where small position changes are no longer captured by the precision of floats
// when the total position is large.
if(abs( target_angle - shaft_angle ) > abs(velocity_limit*Ts)){
shaft_angle += _sign(target_angle - shaft_angle) * abs( velocity_limit )*Ts;
shaft_velocity = velocity_limit;
}else{
shaft_angle = target_angle;
shaft_velocity = 0;
}
// save timestamp for next call
open_loop_timestamp = now_us;
// use voltage limit or current limit
if (torque_controller == TorqueControlType::voltage)
return voltage_limit;
else
return current_limit;
}
// Update limit values in controllers when changed
void FOCMotor::updateVelocityLimit(float new_velocity_limit) {
velocity_limit = new_velocity_limit;
if(controller != MotionControlType::angle_nocascade)
P_angle.limit = abs(velocity_limit); // if angle control but no velocity cascade, limit the angle controller by the velocity limit
}
// Update limit values in controllers when changed
void FOCMotor::updateCurrentLimit(float new_current_limit) {
current_limit = new_current_limit;
if(torque_controller != TorqueControlType::voltage) {
// if current control
PID_velocity.limit = new_current_limit;
if(controller == MotionControlType::angle_nocascade)
// if angle control but no velocity cascade, limit the angle controller by the current limit
P_angle.limit = new_current_limit;
}
}
// Update limit values in controllers when changed
// PID values and limits
void FOCMotor::updateVoltageLimit(float new_voltage_limit) {
voltage_limit = new_voltage_limit;
PID_current_q.limit = new_voltage_limit;
PID_current_d.limit = new_voltage_limit;
if(torque_controller == TorqueControlType::voltage) {
// if voltage control
PID_velocity.limit = new_voltage_limit;
if(controller == MotionControlType::angle_nocascade)
// if angle control but no velocity cascade, limit the angle controller by the voltage limit
P_angle.limit = new_voltage_limit;
}
}
// Update torque control type and related controller limit values
void FOCMotor::updateTorqueControlType(TorqueControlType new_torque_controller) {
torque_controller = new_torque_controller;
// update the
if (torque_controller == TorqueControlType::voltage)
// voltage control
updateVoltageLimit(voltage_limit);
else
// current control
updateCurrentLimit(current_limit);
}
// Update motion control type and related target values
// - if changing to angle control set target to current angle
// - if changing to velocity control set target to zero
// - if changing to torque control set target to zero
void FOCMotor::updateMotionControlType(MotionControlType new_motion_controller) {
if (controller == new_motion_controller) return; // no change
switch(new_motion_controller)
{
case(MotionControlType::angle_nocascade):
if(controller == MotionControlType::angle || controller == MotionControlType::angle_openloop) break;
case MotionControlType::angle:
if(controller == MotionControlType::angle_openloop || controller == MotionControlType::angle_nocascade) break;
case MotionControlType::angle_openloop:
if(controller == MotionControlType::angle || controller == MotionControlType::angle_nocascade) break;
// if the previous controller was not angle control
// set target to current angle
target = shaft_angle;
break;
case MotionControlType::velocity:
if(controller == MotionControlType::velocity_openloop) break; // nothing to do if we are already in velocity control
case MotionControlType::velocity_openloop:
if(controller == MotionControlType::velocity) break;
// if the previous controller was not velocity control
// stop the motor
target = 0;
break;
case MotionControlType::torque:
// if torque control set target to zero
target = 0;
break;
default:
// if torque control set target to zero
target = 0;
break;
}
// finally set the new controller
controller = new_motion_controller;
// update limits in case they need to be changed for the new controller
updateVelocityLimit(velocity_limit);
updateCurrentLimit(current_limit);
updateVoltageLimit(voltage_limit);
}
int FOCMotor::tuneCurrentController(float bandwidth) {
auto old_status = motor_status;
motor_status = FOCMotorStatus::motor_calibrating;
if (bandwidth <= 0.0f) {
// check bandwidth is positive
SIMPLEFOC_MOTOR_ERROR("Fail. BW <= 0");
return 1;
}
if (loopfoc_time_us && bandwidth > 0.5f * (1e6f / loopfoc_time_us)) {
// check bandwidth is not too high for the control loop frequency
SIMPLEFOC_MOTOR_ERROR("Fail. BW too high, current loop freq:" , (1e6f / loopfoc_time_us));
return 2;
}
if (!_isset(phase_resistance) || (!_isset(phase_inductance) && !_isset(axis_inductance.q))) {
// need motor parameters to tune the controller
SIMPLEFOC_MOTOR_WARN("Motor params missing!");
if(characteriseMotor( voltage_sensor_align )) {
return 3;
}
}else if (_isset(phase_inductance) && !(_isset(axis_inductance.q))) {
// if only single inductance value is set, use it for both d and q axis
axis_inductance = {phase_inductance, phase_inductance};
}
PID_current_q.P = axis_inductance.q * (_2PI * bandwidth);
PID_current_q.I = phase_resistance * (_2PI * bandwidth);
PID_current_d.P = _isset(axis_inductance.d) ? axis_inductance.d * (_2PI * bandwidth) : phase_inductance * (_2PI * bandwidth);
PID_current_d.I = phase_resistance * (_2PI * bandwidth);
LPF_current_d.Tf = 1.0f / (_2PI * bandwidth * 5.0f); // filter cutoff at 5x bandwidth
LPF_current_q.Tf = 1.0f / (_2PI * bandwidth * 5.0f); // filter cutoff at 5x bandwidth
SIMPLEFOC_MOTOR_DEBUG("Tunned PI params for BW [Hz]: ", bandwidth);
SIMPLEFOC_MOTOR_DEBUG("Pq: ", PID_current_q.P);
SIMPLEFOC_MOTOR_DEBUG("Iq: ", PID_current_q.I);
SIMPLEFOC_MOTOR_DEBUG("Pd: ", PID_current_d.P);
SIMPLEFOC_MOTOR_DEBUG("Id: ", PID_current_d.I);
motor_status = old_status;
return 0;
}
// Iterative function looping FOC algorithm, setting Uq on the Motor
// The faster it can be run the better
void FOCMotor::loopFOC() {
// update loop time measurement
updateLoopFOCTime();
// if initFOC is being executed at the moment, do nothing
if(motor_status == FOCMotorStatus::motor_calibrating) return;
// update sensor - do this even in open-loop mode, as user may be switching between modes and we could lose track
// of full rotations otherwise.
if (sensor) sensor->update();
// if disabled do nothing
if(!enabled) return;
// if open-loop do nothing
if( controller==MotionControlType::angle_openloop || controller==MotionControlType::velocity_openloop )
// calculate the open loop electirical angle
electrical_angle = _electricalAngle((shaft_angle), pole_pairs);
else
// Needs the update() to be called first
// This function will not have numerical issues because it uses Sensor::getMechanicalAngle()
// which is in range 0-2PI
electrical_angle = electricalAngle();
switch (torque_controller) {
case TorqueControlType::voltage:
voltage.q = _constrain(current_sp, -voltage_limit, voltage_limit) + feed_forward_voltage.q;
voltage.d = feed_forward_voltage.d;
break;
case TorqueControlType::estimated_current:
if(! _isset(phase_resistance)) return;
// constrain current setpoint
current_sp = _constrain(current_sp, -current_limit, current_limit) + feed_forward_current.q; // desired current is the setpoint
// calculate the back-emf voltage if KV_rating available U_bemf = vel*(1/KV)
if (_isset(KV_rating)) voltage_bemf = estimateBEMF(shaft_velocity);
// filter the value values
current.q = LPF_current_q(current_sp);
// calculate the phase voltage
voltage.q = current.q * phase_resistance + voltage_bemf;
// constrain voltage within limits
voltage.q = _constrain(voltage.q, -voltage_limit, voltage_limit) + feed_forward_voltage.q;
// d voltage - lag compensation
if(_isset(axis_inductance.q)) voltage.d = _constrain( -current_sp*shaft_velocity*pole_pairs*axis_inductance.q, -voltage_limit, voltage_limit) + feed_forward_voltage.d;
else voltage.d = feed_forward_voltage.d;
break;
case TorqueControlType::dc_current:
if(!current_sense) return;
// constrain current setpoint
current_sp = _constrain(current_sp, -current_limit, current_limit) + feed_forward_current.q;
// read overall current magnitude
current.q = current_sense->getDCCurrent(electrical_angle);
// filter the value values
current.q = LPF_current_q(current.q);
// calculate the phase voltage
voltage.q = PID_current_q(current_sp - current.q) + feed_forward_voltage.q;
// d voltage - lag compensation
if(_isset(axis_inductance.q)) voltage.d = _constrain( -current_sp*shaft_velocity*pole_pairs*axis_inductance.q, -voltage_limit, voltage_limit) + feed_forward_voltage.d;
else voltage.d = feed_forward_voltage.d;
break;
case TorqueControlType::foc_current:
if(!current_sense) return;
// constrain current setpoint
current_sp = _constrain(current_sp, -current_limit, current_limit) + feed_forward_current.q;
// read dq currents
current = current_sense->getFOCCurrents(electrical_angle);
// filter values
current.q = LPF_current_q(current.q);
current.d = LPF_current_d(current.d);
// calculate the phase voltages
voltage.q = PID_current_q(current_sp - current.q);
voltage.d = PID_current_d(feed_forward_current.d - current.d);
// d voltage - lag compensation
//if(_isset(axis_inductance.q)) voltage.d = _constrain( voltage.d - current_sp*shaft_velocity*pole_pairs*axis_inductance.q, -voltage_limit, voltage_limit);
// q voltage - cross coupling compensation - TODO verify
//if(_isset(axis_inductance.d)) voltage.q = _constrain( voltage.q + current.d*shaft_velocity*pole_pairs*axis_inductance.d, -voltage_limit, voltage_limit);
// add feed forward
voltage.q += feed_forward_voltage.q;
voltage.d += feed_forward_voltage.d;
break;
default:
// no torque control selected
SIMPLEFOC_MOTOR_ERROR("no torque control selected!");
break;
}
// set the phase voltage - FOC heart function :)
setPhaseVoltage(voltage.q, voltage.d, electrical_angle);
}
// Iterative function running outer loop of the FOC algorithm
// Behavior of this function is determined by the motor.controller variable
// It runs either angle, velocity or voltage loop
// - needs to be called iteratively it is asynchronous function
// - if target is not set it uses motor.target value
void FOCMotor::move(float new_target) {
// set internal target variable
if(_isset(new_target) ) target = new_target;
// downsampling (optional)
if(motion_cnt++ < motion_downsample) return;
motion_cnt = 0;
// if initFOC is being executed at the moment, do nothing
if(motor_status == FOCMotorStatus::motor_calibrating) return;
// calculate the elapsed time between the calls
// TODO replace downsample by runnind the code at
// a specific frequency (or almost)
updateMotionControlTime();
// read value even if motor is disabled to keep the monitoring updated
// except for the open loop modes where the values are updated within angle/velocityOpenLoop functions
// shaft angle/velocity need the update() to be called first
// get shaft angle
// TODO sensor precision: the shaft_angle actually stores the complete position, including full rotations, as a float
// For this reason it is NOT precise when the angles become large.
// Additionally, the way LPF works on angle is a precision issue, and the angle-LPF is a problem
// when switching to a 2-component representation.
if( controller!=MotionControlType::angle_openloop && controller!=MotionControlType::velocity_openloop ){
// read the values only if the motor is not in open loop
// because in open loop the shaft angle/velocity is updated within angle/velocityOpenLoop functions
shaft_angle = shaftAngle();
shaft_velocity = shaftVelocity();
}
// if disabled do nothing
// and if
if(!enabled) return;
// upgrade the current based voltage limit
switch (controller) {
case MotionControlType::torque:
current_sp = target;
break;
case MotionControlType::angle_nocascade:
// TODO sensor precision: this calculation is not numerically precise. The target value cannot express precise positions when
// the angles are large. This results in not being able to command small changes at high position values.
// to solve this, the delta-angle has to be calculated in a numerically precise way.
// angle set point
shaft_angle_sp = target;
// calculate the torque command - sensor precision: this calculation is ok, but based on bad value from previous calculation
current_sp = P_angle(shaft_angle_sp - LPF_angle(shaft_angle));
break;
case MotionControlType::angle:
// TODO sensor precision: this calculation is not numerically precise. The target value cannot express precise positions when
// the angles are large. This results in not being able to command small changes at high position values.
// to solve this, the delta-angle has to be calculated in a numerically precise way.
// angle set point
shaft_angle_sp = target;
// calculate velocity set point
shaft_velocity_sp = feed_forward_velocity + P_angle( shaft_angle_sp - LPF_angle(shaft_angle) );
shaft_velocity_sp = _constrain(shaft_velocity_sp, -velocity_limit, velocity_limit);
// calculate the torque command - sensor precision: this calculation is ok, but based on bad value from previous calculation
current_sp = PID_velocity(shaft_velocity_sp - shaft_velocity);
break;
case MotionControlType::velocity:
// velocity set point - sensor precision: this calculation is numerically precise.
shaft_velocity_sp = target;
// calculate the torque command
current_sp = PID_velocity(shaft_velocity_sp - shaft_velocity);
break;
case MotionControlType::velocity_openloop:
// velocity control in open loop - sensor precision: this calculation is numerically precise.
shaft_velocity_sp = target;
// this function updates the shaft_angle and shaft_velocity
// returns the voltage or current that is to be set to the motor (depending on torque control mode)
// returned values correspond to the voltage_limit and current_limit
current_sp = velocityOpenloop(shaft_velocity_sp);
break;
case MotionControlType::angle_openloop:
// angle control in open loop -
// TODO sensor precision: this calculation NOT numerically precise, and subject
// to the same problems in small set-point changes at high angles
// as the closed loop version.
shaft_angle_sp = target;
// this function updates the shaft_angle and shaft_velocity
// returns the voltage or current that is to be set to the motor (depending on torque control mode)
// returned values correspond to the voltage_limit and current_limit
current_sp = angleOpenloop(shaft_angle_sp);
break;
case MotionControlType::custom:
// custom control - user provides the function that calculates the current_sp
// based on the target value and the motor state
// user makes sure to use it with appropriate torque control mode
if(customMotionControlCallback)
current_sp = customMotionControlCallback(this);
break;
}
}
// FOC initialization function
int FOCMotor::initFOC() {
int exit_flag = 1;
motor_status = FOCMotorStatus::motor_calibrating;
// align motor if necessary
// alignment necessary for encoders!
// sensor and motor alignment - can be skipped
// by setting motor.sensor_direction and motor.zero_electric_angle
if(sensor){
exit_flag *= alignSensor();
// added the shaft_angle update
sensor->update();
shaft_angle = shaftAngle();
} else {
SIMPLEFOC_MOTOR_DEBUG("No sensor.");
if ((controller == MotionControlType::angle_openloop || controller == MotionControlType::velocity_openloop)){
exit_flag = 1;
}else{
SIMPLEFOC_MOTOR_ERROR("Only openloop allowed!");
exit_flag = 0; // no FOC without sensor
}
}
// aligning the current sensor - can be skipped
// checks if driver phases are the same as current sense phases
// and checks the direction of measuremnt.
if(exit_flag){
if(current_sense){
if (!current_sense->initialized) {
motor_status = FOCMotorStatus::motor_calib_failed;
SIMPLEFOC_MOTOR_ERROR("Current sense not init!");
exit_flag = 0;
}else{
exit_flag *= alignCurrentSense();
}
}
else { SIMPLEFOC_MOTOR_ERROR("No current sense."); }
}
if(exit_flag){
SIMPLEFOC_MOTOR_DEBUG("Ready.");
motor_status = FOCMotorStatus::motor_ready;
}else{
SIMPLEFOC_MOTOR_ERROR("Init FOC fail");
motor_status = FOCMotorStatus::motor_calib_failed;
disable();
}
return exit_flag;
}
// Calibarthe the motor and current sense phases
int FOCMotor::alignCurrentSense() {
int exit_flag = 1; // success
SIMPLEFOC_MOTOR_DEBUG("Align current sense.");
// align current sense and the driver
exit_flag = current_sense->driverAlign(voltage_sensor_align, modulation_centered);
if(!exit_flag){
// error in current sense - phase either not measured or bad connection
SIMPLEFOC_MOTOR_ERROR("Align error!");
exit_flag = 0;
}else{
// output the alignment status flag
SIMPLEFOC_MOTOR_DEBUG("Success: ", exit_flag);
}
return exit_flag > 0;
}
// Encoder alignment to electrical 0 angle
int FOCMotor::alignSensor() {
int exit_flag = 1; // success
SIMPLEFOC_MOTOR_DEBUG("Align sensor.");
// check if sensor needs zero search
if(sensor->needsSearch()) exit_flag = absoluteZeroSearch();
// stop init if not found index
if(!exit_flag) return exit_flag;
// v2.3.3 fix for R_AVR_7_PCREL against symbol" bug for AVR boards
// TODO figure out why this works
float voltage_align = voltage_sensor_align;
// if unknown natural direction
if(sensor_direction == Direction::UNKNOWN){
// find natural direction
// move one electrical revolution forward
for (int i = 0; i <=500; i++ ) {
float angle = _3PI_2 + _2PI * i / 500.0f;
setPhaseVoltage(voltage_align, 0, angle);
sensor->update();
_delay(2);
}
// take and angle in the middle
sensor->update();
float mid_angle = sensor->getAngle();
// move one electrical revolution backwards
for (int i = 500; i >=0; i-- ) {
float angle = _3PI_2 + _2PI * i / 500.0f ;
setPhaseVoltage(voltage_align, 0, angle);
sensor->update();
_delay(2);
}
sensor->update();
float end_angle = sensor->getAngle();
// setPhaseVoltage(0, 0, 0);
_delay(200);
// determine the direction the sensor moved
float moved = fabsf(mid_angle - end_angle);
if (moved<MIN_ANGLE_DETECT_MOVEMENT) { // minimum angle to detect movement
SIMPLEFOC_MOTOR_ERROR("Failed to notice movement");
return 0; // failed calibration
} else if (mid_angle < end_angle) {
SIMPLEFOC_MOTOR_DEBUG("sensor dir: CCW");
sensor_direction = Direction::CCW;
} else{
SIMPLEFOC_MOTOR_DEBUG("sensor dir: CW");
sensor_direction = Direction::CW;
}
// check pole pair number
pp_check_result = !(fabsf(moved*pole_pairs - _2PI) > 0.5f); // 0.5f is arbitrary number it can be lower or higher!
if( pp_check_result==false ) {
SIMPLEFOC_MOTOR_WARN("PP check: fail - est. pp: ", _2PI/moved);
} else {
SIMPLEFOC_MOTOR_DEBUG("PP check: OK!");
}
} else SIMPLEFOC_MOTOR_DEBUG("Skip dir calib.");
// zero electric angle not known
if(!_isset(zero_electric_angle)){
// align the electrical phases of the motor and sensor
// set angle -90(270 = 3PI/2) degrees
setPhaseVoltage(voltage_align, 0, _3PI_2);
_delay(700);
// read the sensor
sensor->update();
// get the current zero electric angle
zero_electric_angle = 0;
zero_electric_angle = electricalAngle();
_delay(20);
SIMPLEFOC_MOTOR_DEBUG("Zero elec. angle: ", zero_electric_angle);
// stop everything
setPhaseVoltage(0, 0, 0);
_delay(200);
} else { SIMPLEFOC_MOTOR_DEBUG("Skip offset calib."); }
return exit_flag;
}
// Encoder alignment the absolute zero angle
// - to the index
int FOCMotor::absoluteZeroSearch() {
// sensor precision: this is all ok, as the search happens near the 0-angle, where the precision
// of float is sufficient.
SIMPLEFOC_MOTOR_DEBUG("Index search...");
// search the absolute zero with small velocity
float limit_vel = velocity_limit;
float limit_volt = voltage_limit;
velocity_limit = velocity_index_search;
voltage_limit = voltage_sensor_align;
shaft_angle = 0;
// angle equivalent for a 1.5 mechanical rotation
float search_rotation_target = 1.5f*_2PI;
while(sensor->needsSearch() && shaft_angle < search_rotation_target){
// updates shaft angle
angleOpenloop(search_rotation_target);
// call important for some sensors not to loose count
// not needed for the search
sensor->update();
// set the voltage to the motor
setPhaseVoltage(voltage_limit, 0, _electricalAngle(shaft_angle, pole_pairs));
}
// disable motor
setPhaseVoltage(0, 0, 0);
// reinit the limits
velocity_limit = limit_vel;
voltage_limit = limit_volt;
// check if the zero found
if(monitor_port){
if(sensor->needsSearch()) { SIMPLEFOC_MOTOR_ERROR("Not found!"); }
else { SIMPLEFOC_MOTOR_DEBUG("Success!"); }
}
return !sensor->needsSearch();
}