From bae5423ea6db88d9e14e8c0c7467b4994ba23f3c Mon Sep 17 00:00:00 2001 From: gospar Date: Fri, 5 Dec 2025 08:53:55 +0100 Subject: [PATCH 1/3] u16 lut --- src/encoders/calibrated/CalibratedSensor.cpp | 117 ++++++++++--------- src/encoders/calibrated/CalibratedSensor.h | 25 +++- src/encoders/calibrated/README.md | 22 +++- 3 files changed, 104 insertions(+), 60 deletions(-) diff --git a/src/encoders/calibrated/CalibratedSensor.cpp b/src/encoders/calibrated/CalibratedSensor.cpp index 089b17a..2466219 100644 --- a/src/encoders/calibrated/CalibratedSensor.cpp +++ b/src/encoders/calibrated/CalibratedSensor.cpp @@ -3,8 +3,10 @@ // CalibratedSensor() // sensor - instance of original sensor object // n_lut - number of samples in the LUT -CalibratedSensor::CalibratedSensor(Sensor &wrapped, int n_lut, float *lut) +CalibratedSensor::CalibratedSensor(Sensor &wrapped, int n_lut, uint16_t *lut) : _wrapped(wrapped), n_lut(n_lut), allocated(false), calibrationLut(lut) { + lut_resolution = n_lut / _2PI; + lut_resolution_inv = 1/ lut_resolution; }; CalibratedSensor::~CalibratedSensor() { @@ -34,28 +36,29 @@ float CalibratedSensor::getSensorAngle() if(!calibrationLut) { return _wrapped.getMechanicalAngle(); } + // raw encoder position e.g. 0-2PI - float raw_angle = fmodf(_wrapped.getMechanicalAngle(), _2PI); - raw_angle += raw_angle < 0 ? _2PI:0; + float raw_angle = _wrapped.getMechanicalAngle(); + // wrap to 0-2PI only if needed (for Encoder sensors that can go beyond 2PI) + if (raw_angle < 0 || raw_angle >= _2PI) raw_angle = _normalizeAngle(raw_angle); - // Calculate the resolution of the LUT in radians - float lut_resolution = _2PI / n_lut; // Calculate LUT index - int lut_index = raw_angle / lut_resolution; + int lut_index = raw_angle * lut_resolution_inv; - // Get calibration values from the LUT - float y0 = calibrationLut[lut_index]; - float y1 = calibrationLut[(lut_index + 1) % n_lut]; + // Get calibration values from the LUT and decode them + float lut_entry_lower = decodeOffsetU16(calibrationLut[lut_index]); + float lut_entry_higher = decodeOffsetU16(calibrationLut[ lut_index >= n_lut-1 ? 0 : (lut_index + 1)]); - // Linearly interpolate between the y0 and y1 values - // Calculate the relative distance from the y0 (raw_angle has to be between y0 and y1) - // If distance = 0, interpolated offset = y0 - // If distance = 1, interpolated offset = y1 - float distance = (raw_angle - lut_index * lut_resolution) / lut_resolution; - float offset = (1 - distance) * y0 + distance * y1; + // Linearly interpolate between the two closest LUT entries (one lower and one higher than the raw angle) + // Calculate the distance between the raw angle and the lower LUT entry + // Distance is normalized to [0,1] + float lut_lower_angle = lut_index * lut_resolution; + float distance_lower = (raw_angle - lut_lower_angle) * lut_resolution_inv; + // Linearly interpolate between lower and higher LUT entries + float correction_offset = (1.0f - distance_lower) * lut_entry_lower + distance_lower * lut_entry_higher; // Calculate the calibrated angle - return raw_angle - offset; + return raw_angle - correction_offset; } // Perform filtering to linearize position sensor eccentricity @@ -98,9 +101,12 @@ void CalibratedSensor::calibrate(FOCMotor &motor, int settle_time_ms) if(calibrationLut == NULL) { allocated = true; - calibrationLut = new float[n_lut]; + calibrationLut = new uint16_t[n_lut]; + }else{ + SIMPLEFOC_DEBUG("SEN_CAL:Using pre-defined LUT for calibration."); + return; } - motor.monitor_port->println("Starting Sensor Calibration."); + SIMPLEFOC_DEBUG("SEN_CAL:Starting Sensor Calibration."); // Calibration variables @@ -136,7 +142,7 @@ void CalibratedSensor::calibrate(FOCMotor &motor, int settle_time_ms) motor.current_sense = nullptr; motor.linkSensor(&this->_wrapped); if(!motor.initFOC()){ - motor.monitor_port->println("Failed to align the sensor."); + SIMPLEFOC_DEBUG("SEN_CAL: Failed to align the sensor."); return; } // link back the sensor and current sense @@ -160,8 +166,7 @@ void CalibratedSensor::calibrate(FOCMotor &motor, int settle_time_ms) /* forwards rotation */ - motor.monitor_port->print("Rotating: "); - motor.monitor_port->println( motor.sensor_direction == Direction::CCW ? "CCW" : "CW" ); + SIMPLEFOC_DEBUG(motor.sensor_direction == Direction::CCW ? "Rotating: CCW" : "Rotating: CW" ); float zero_angle_prev = 0.0; for (int i = 0; i < n_ticks; i++) { @@ -191,18 +196,17 @@ void CalibratedSensor::calibrate(FOCMotor &motor, int settle_time_ms) zero_angle_prev = zero_angle; avg_elec_angle += zero_angle/n_ticks; - // motor.monitor_port->print(">zero:"); - // motor.monitor_port->println(zero_angle); - // motor.monitor_port->print(">zero_average:"); - // motor.monitor_port->println(avg_elec_angle/2.0); +#ifdef SIMPLEFOC_CALIBRATEDSENSOR_DEBUG + SIMPLEFOC_DEBUG(">zero:",zero_angle); + SIMPLEFOC_DEBUG(">zero_average:", (float)(avg_elec_angle/2.0)); +#endif } _delay(2000); /* backwards rotation */ - motor.monitor_port->print("Rotating: "); - motor.monitor_port->println( motor.sensor_direction == Direction::CCW ? "CW" : "CCW" ); + SIMPLEFOC_DEBUG(motor.sensor_direction == Direction::CCW ? "Rotating: CW" : "Rotating: CCW" ); for (int i = n_ticks - 1; i >= 0; i--) { for (int j = 0; j < n2_ticks; j++) // move to the next location @@ -229,11 +233,10 @@ void CalibratedSensor::calibrate(FOCMotor &motor, int settle_time_ms) } zero_angle_prev = zero_angle; avg_elec_angle += zero_angle/n_ticks; - - // motor.monitor_port->print(">zero:"); - // motor.monitor_port->println(zero_angle); - // motor.monitor_port->print(">zero_average:"); - // motor.monitor_port->println(avg_elec_angle/2.0); +#ifdef SIMPLEFOC_CALIBRATEDSENSOR_DEBUG + SIMPLEFOC_DEBUG(">zero:", zero_angle); + SIMPLEFOC_DEBUG(">zero_average:", (float)(avg_elec_angle/2.0)); +#endif } // get post calibration mechanical angle. @@ -248,8 +251,7 @@ void CalibratedSensor::calibrate(FOCMotor &motor, int settle_time_ms) // calculating the average zero electrical angle from the forward calibration. motor.zero_electric_angle = _normalizeAngle(avg_elec_angle / (2.0)); - motor.monitor_port->print("Average Zero Electrical Angle: "); - motor.monitor_port->println(motor.zero_electric_angle); + SIMPLEFOC_DEBUG("SEN_CAL:Average Zero Electrical Angle: ", motor.zero_electric_angle); _delay(1000); // Perform filtering to linearize position sensor eccentricity @@ -264,7 +266,7 @@ void CalibratedSensor::calibrate(FOCMotor &motor, int settle_time_ms) int index_offset = floor((float)n_lut * raw_offset / _2PI); float dn = n_ticks / (float)n_lut; - motor.monitor_port->println("Constructing LUT."); + SIMPLEFOC_DEBUG("SEN_CAL: Constructing LUT."); _delay(1000); // Build Look Up Table for (int i = 0; i < n_lut; i++) @@ -272,36 +274,45 @@ void CalibratedSensor::calibrate(FOCMotor &motor, int settle_time_ms) int ind = index_offset + i*motor.sensor_direction; if (ind > (n_lut - 1)) ind -= n_lut; if (ind < 0) ind += n_lut; - calibrationLut[ind] = (float)(error[(int)(i * dn)] - error_mean); + float offset_value = (float)(error[(int)(i * dn)] - error_mean); // negate the error if the sensor is in the opposite direction - calibrationLut[ind] = (int)motor.sensor_direction * calibrationLut[ind]; + offset_value = (int)motor.sensor_direction * offset_value; + // encode to uint16_t + calibrationLut[ind] = encodeOffsetU16(offset_value); } - motor.monitor_port->println(""); _delay(1000); + SIMPLEFOC_DEBUG("SEN_CAL: Sensor Calibration Done."); + + // pre-compute inverse LUT resolution for faster lookups + lut_resolution = _2PI / n_lut; + lut_resolution_inv = 1.0 / lut_resolution; +} + +// print the LUT for debugging +void CalibratedSensor::printLUT(FOCMotor &motor, Print &printer) +{ // Display the LUT - motor.monitor_port->print("float calibrationLut["); - motor.monitor_port->print(n_lut); - motor.monitor_port->println("] = {"); + printer.print("uint16_t calibrationLut["); + printer.print(n_lut); + printer.println("] = {"); _delay(100); for (int i=0;i < n_lut; i++){ - motor.monitor_port->print(calibrationLut[i],6); - if(i < n_lut - 1) motor.monitor_port->print(", "); + printer.print(calibrationLut[i]); + if(i < n_lut - 1) printer.print(", "); _delay(1); } - motor.monitor_port->println("};"); + printer.println(""); + printer.println("};"); _delay(1000); // Display the zero electrical angle - motor.monitor_port->print("float zero_electric_angle = "); - motor.monitor_port->print(motor.zero_electric_angle,6); - motor.monitor_port->println(";"); + printer.print("float zero_electric_angle = "); + printer.print(motor.zero_electric_angle); + printer.println(";"); // Display the sensor direction - motor.monitor_port->print("Direction sensor_direction = "); - motor.monitor_port->println(motor.sensor_direction == Direction::CCW ? "Direction::CCW;" : "Direction::CW;"); + printer.print("Direction sensor_direction = "); + printer.println(motor.sensor_direction == Direction::CCW ? "Direction::CCW;" : "Direction::CW;"); _delay(1000); - - motor.monitor_port->println("Sensor Calibration Done."); -} - +} \ No newline at end of file diff --git a/src/encoders/calibrated/CalibratedSensor.h b/src/encoders/calibrated/CalibratedSensor.h index 29a0a8d..d61977f 100644 --- a/src/encoders/calibrated/CalibratedSensor.h +++ b/src/encoders/calibrated/CalibratedSensor.h @@ -5,7 +5,12 @@ #include "BLDCMotor.h" #include "common/base_classes/FOCMotor.h" #include "common/foc_utils.h" +#include "communication/SimpleFOCDebug.h" +// LUT quantization constants for uint16_t encoding +// Maps radians in range [-PI, PI] to uint16_t [0, 65535] +#define LUT_SCALE 10430.2191955f // 65535 / (2*PI) +#define LUT_OFFSET 32767.0f // center value for symmetric range class CalibratedSensor: public Sensor{ @@ -16,7 +21,7 @@ class CalibratedSensor: public Sensor{ * @param n_lut the number of entries in the lut * @param lut the look up table (if null, the lut will be allocated on the heap) */ - CalibratedSensor(Sensor& wrapped, int n_lut = 200, float* lut = nullptr); + CalibratedSensor(Sensor& wrapped, int n_lut = 200, uint16_t* lut = nullptr); ~CalibratedSensor(); @@ -30,6 +35,15 @@ class CalibratedSensor: public Sensor{ */ virtual void calibrate(FOCMotor& motor, int settle_time_ms = 30); + /** + * Print the LUT to the provided Print instance + * @param motor the FOCMotor instance (for direction info) + * @param printer the Print instance to print to + * + * Print in the form of C++ code array for easy copy-pasting + */ + void printLUT(FOCMotor& motor, Print &printer); + // voltage to run the calibration: user input float voltage_calibration = 1; protected: @@ -58,7 +72,14 @@ class CalibratedSensor: public Sensor{ // depending on the size of the lut // will be allocated in the calibrate function if not given. bool allocated; - float* calibrationLut; + uint16_t* calibrationLut; + // pre-computed inverse LUT resolution for faster lookups: n_lut / (2*PI) + float lut_resolution_inv { 0.0f }; + float lut_resolution { 0.0f }; + + // Helper functions for quantization + inline uint16_t encodeOffsetU16(float offset) { return (uint16_t)(offset * LUT_SCALE + LUT_OFFSET); } + inline float decodeOffsetU16(uint16_t encoded) { return (encoded - LUT_OFFSET) / LUT_SCALE; } }; #endif diff --git a/src/encoders/calibrated/README.md b/src/encoders/calibrated/README.md index 9a1c823..b2e5198 100644 --- a/src/encoders/calibrated/README.md +++ b/src/encoders/calibrated/README.md @@ -102,7 +102,7 @@ Rotating: CW Average Zero Electrical Angle: 4.01 Constructing LUT. -float calibrationLut[50] = {0.003486, 0.005795, 0.007298, 0.008303, 0.008771, 0.007551, 0.005986, 0.004115, 0.001361, -0.001392, -0.004069, -0.007474, -0.010420, -0.013135, -0.014891, -0.017415, -0.018328, -0.019125, -0.018849, -0.017193, -0.015152, -0.012422, -0.008579, -0.003970, 0.000678, 0.005211, 0.009821, 0.013280, 0.016470, 0.018127, 0.018376, 0.016969, 0.016716, 0.015466, 0.013602, 0.011431, 0.008646, 0.006092, 0.003116, 0.000409, -0.002342, -0.004367, -0.005932, -0.006998, -0.007182, -0.007175, -0.006017, -0.003746, -0.001783, 0.000948}; +uint16_t calibrationLut[50] = {32803, 32828, 32843, 32854, 32859, 32846, 32830, 32810, 32781, 32753, 32725, 32689, 32658, 32631, 32612, 32586, 32576, 32568, 32571, 32588, 32609, 32638, 32678, 32726, 32775, 32822, 32870, 32906, 32939, 32956, 32959, 32944, 32941, 32929, 32909, 32887, 32858, 32831, 32800, 32771, 32743, 32722, 32706, 32694, 32693, 32693, 32705, 32728, 32749, 32777}; float zero_electric_angle = 4.007072; Direction sensor_direction = Direction::CCW; Sensor Calibration Done @@ -117,8 +117,9 @@ Your code will look something like this: // number of LUT entries const N_LUT = 50; -// Lookup table that has been ouptut from the calibration process -float calibrationLut[50] = {0.003486, 0.005795, 0.007298, 0.008303, 0.008771, 0.007551, 0.005986, 0.004115, 0.001361, -0.001392, -0.004069, -0.007474, -0.010420, -0.013135, -0.014891, -0.017415, -0.018328, -0.019125, -0.018849, -0.017193, -0.015152, -0.012422, -0.008579, -0.003970, 0.000678, 0.005211, 0.009821, 0.013280, 0.016470, 0.018127, 0.018376, 0.016969, 0.016716, 0.015466, 0.013602, 0.011431, 0.008646, 0.006092, 0.003116, 0.000409, -0.002342, -0.004367, -0.005932, -0.006998, -0.007182, -0.007175, -0.006017, -0.003746, -0.001783, 0.000948}; +// Lookup table that has been output from the calibration process +// The LUT is now stored as uint16_t for 50% memory savings (2 bytes vs 4 bytes per entry) +uint16_t calibrationLut[50] = {32803, 32828, 32843, 32854, 32859, 32846, 32830, 32810, 32781, 32753, 32725, 32689, 32658, 32631, 32612, 32586, 32576, 32568, 32571, 32588, 32609, 32638, 32678, 32726, 32775, 32822, 32870, 32906, 32939, 32956, 32959, 32944, 32941, 32929, 32909, 32887, 32858, 32831, 32800, 32771, 32743, 32722, 32706, 32694, 32693, 32693, 32705, 32728, 32749, 32777}; float zero_electric_angle = 4.007072; Direction sensor_direction = Direction::CCW; @@ -143,7 +144,18 @@ void setup() { ``` +## Implementation Details + +### Memory Optimization + +The LUT is now stored using `uint16_t` instead of `float`, providing a 50% reduction in memory usage: +- **Old**: 4 bytes per entry (float) +- **New**: 2 bytes per entry (uint16_t) +- **Example**: For a 200-point LUT: 800 bytes → 400 bytes + +The quantization maps the offset range [-π, π] radians to [0, 65535], providing a resolution of approximately 0.0001 radians (0.0057°), which is more than sufficient for motor control applications. + ## Future work -- Reduce the LUT size by using a more efficient LUT type - maybe pass to uint16_t -- Use a more eficient LUT interpolation method - maybe a polynomial interpolation \ No newline at end of file +- Use a more efficient LUT interpolation method - maybe a polynomial interpolation +- Support for saving/loading LUT to/from persistent storage (EEPROM, Flash) From 49f027c0a95535710972aa0409ac87e916d61400 Mon Sep 17 00:00:00 2001 From: gospar Date: Fri, 5 Dec 2025 10:49:58 +0100 Subject: [PATCH 2/3] update the readme and comments --- src/encoders/calibrated/CalibratedSensor.cpp | 117 +++++++++-------- src/encoders/calibrated/CalibratedSensor.h | 5 +- src/encoders/calibrated/README.md | 126 +++++++++++-------- 3 files changed, 142 insertions(+), 106 deletions(-) diff --git a/src/encoders/calibrated/CalibratedSensor.cpp b/src/encoders/calibrated/CalibratedSensor.cpp index 2466219..1534678 100644 --- a/src/encoders/calibrated/CalibratedSensor.cpp +++ b/src/encoders/calibrated/CalibratedSensor.cpp @@ -65,24 +65,24 @@ float CalibratedSensor::getSensorAngle() // FIR n-sample average, where n = number of samples in the window // This filter has zero gain at electrical frequency and all integer multiples // So cogging effects should be completely filtered out -void CalibratedSensor::filter_error(float* error, float &error_mean, int n_ticks, int window){ +void CalibratedSensor::filter_error(float* error, float &error_mean, int samples_per_full_rotation, int window){ float window_buffer[window]; memset(window_buffer, 0, window*sizeof(float)); float window_sum = 0; int buffer_index = 0; // fill the inital window buffer for (int i = 0; i < window; i++) { - int ind = n_ticks - window/2 -1 + i; - window_buffer[i] = error[ind % n_ticks]; + int ind = samples_per_full_rotation - window/2 -1 + i; + window_buffer[i] = error[ind % samples_per_full_rotation]; window_sum += window_buffer[i]; } // calculate the moving average error_mean = 0; - for (int i = 0; i < n_ticks; i++) + for (int i = 0; i < samples_per_full_rotation; i++) { // Update buffer window_sum -= window_buffer[buffer_index]; - window_buffer[buffer_index] = error[( i + window/2 ) %n_ticks]; + window_buffer[buffer_index] = error[( i + window/2 ) %samples_per_full_rotation]; window_sum += window_buffer[buffer_index]; // update the buffer index buffer_index = (buffer_index + 1) % window; @@ -90,7 +90,7 @@ void CalibratedSensor::filter_error(float* error, float &error_mean, int n_ticks // Update filtered error error[i] = window_sum / (float)window; // update the mean value - error_mean += error[i] / n_ticks; + error_mean += error[i] / samples_per_full_rotation; } } @@ -103,10 +103,10 @@ void CalibratedSensor::calibrate(FOCMotor &motor, int settle_time_ms) allocated = true; calibrationLut = new uint16_t[n_lut]; }else{ - SIMPLEFOC_DEBUG("SEN_CAL:Using pre-defined LUT for calibration."); + SIMPLEFOC_DEBUG("SEN_CAL: Using pre-defined LUT for calibration."); return; } - SIMPLEFOC_DEBUG("SEN_CAL:Starting Sensor Calibration."); + SIMPLEFOC_DEBUG("SEN_CAL: Starting Sensor Calibration."); // Calibration variables @@ -116,21 +116,30 @@ void CalibratedSensor::calibrate(FOCMotor &motor, int settle_time_ms) // set the inital electric angle to 0 float elec_angle = 0.0; - // Calibration parameters - // The motor will take a n_pos samples per electrical cycle - // which amounts to n_ticks (n_pos * motor.pole_pairs) samples per mechanical rotation - // Additionally, the motor will take n2_ticks steps to reach any of the n_ticks posiitons - // incrementing the electrical angle by deltaElectricalAngle each time - int n_pos = 5; - int _NPP = motor.pole_pairs; // number of pole pairs which is user input - const int n_ticks = n_pos * _NPP; // number of positions to be sampled per mechanical rotation. Multiple of NPP for filtering reasons (see later) - const int n2_ticks = 5; // increments between saved samples (for smoothing motion) - float deltaElectricalAngle = _2PI * _NPP / (n_ticks * n2_ticks); // Electrical Angle increments for calibration steps - float error[n_ticks]; // pointer to error array (average of forward & backward) - memset(error, 0, n_ticks*sizeof(float)); - // The fileter window size is set to n_pos - one electrical cycle - // important for cogging filtering !!! - const int window = n_pos; // window size for moving average filter of raw error + /* + * Calibration procedure + * The calibration will rotate the motor shaft in small steps in total n_lut positions (in both directions) + * It will stop at each of these positions and read the sensor angle + * The error between the expected angle (from the motor electrical angle) and the actual angle + * (from the sensor) is stored in an array + */ + + // number of pole pairs which is user input + const int pole_pairs = motor.pole_pairs; + // number of positions per electrical cycle (If LUT size is not multiple of NPP, it is rounded up) + const int samples_per_elec_rotation = ceil((float)n_lut / (float)pole_pairs); + // number of positions to be sampled per mechanical rotation. + // Ideally it would be equal to the LUT size, but if the LUT size is not multiple of NPP, it is rounded up + const int samples_per_full_rotation = samples_per_elec_rotation * pole_pairs; + // pointer to error array (average of forward & backward) + float error[samples_per_full_rotation]; + memset(error, 0, samples_per_full_rotation*sizeof(float)); + + // Additional parameters for calibration loop + // total number of position to iterate through between each sampling position (to reduce jumps if LUT size is small) + const int intermeditate_positions = 5; + // Electrical Angle increments for calibration steps + float el_angle_increment = _2PI * pole_pairs / (samples_per_full_rotation * intermeditate_positions); // find the first guess of the motor.zero_electric_angle @@ -159,21 +168,21 @@ void CalibratedSensor::calibrate(FOCMotor &motor, int settle_time_ms) /* Start Calibration - Loop over electrical angles from 0 to NPP*2PI, once forward, once backward + Loop over electrical angles from 0 to pole_pairs*2PI, once forward, once backward store actual position and error as compared to electrical angle */ /* forwards rotation */ - SIMPLEFOC_DEBUG(motor.sensor_direction == Direction::CCW ? "Rotating: CCW" : "Rotating: CW" ); + SIMPLEFOC_DEBUG(motor.sensor_direction == Direction::CCW ? "SEN_CAL: Rotating: CCW" : "SEN_CAL: Rotating: CW" ); float zero_angle_prev = 0.0; - for (int i = 0; i < n_ticks; i++) + for (int i = 0; i < samples_per_full_rotation; i++) { - for (int j = 0; j < n2_ticks; j++) // move to the next location + for (int j = 0; j < intermeditate_positions; j++) // move to the next location { _wrapped.update(); - elec_angle += deltaElectricalAngle; + elec_angle += el_angle_increment; motor.setPhaseVoltage(voltage_calibration, 0, elec_angle); } @@ -182,10 +191,10 @@ void CalibratedSensor::calibrate(FOCMotor &motor, int settle_time_ms) _wrapped.update(); // calculate the error theta_actual = (int)motor.sensor_direction * (_wrapped.getAngle() - theta_init); - error[i] = 0.5 * (theta_actual - elec_angle / _NPP); + error[i] = 0.5 * (theta_actual - elec_angle / pole_pairs); // calculate the current electrical zero angle - float zero_angle = ((int)motor.sensor_direction * _wrapped.getMechanicalAngle() * _NPP ) - (elec_angle + _PI_2); + float zero_angle = ((int)motor.sensor_direction * _wrapped.getMechanicalAngle() * pole_pairs ) - (elec_angle + _PI_2); zero_angle = _normalizeAngle(zero_angle); // remove the 2PI jumps if(zero_angle - zero_angle_prev > _PI){ @@ -194,11 +203,11 @@ void CalibratedSensor::calibrate(FOCMotor &motor, int settle_time_ms) zero_angle = zero_angle + _2PI; } zero_angle_prev = zero_angle; - avg_elec_angle += zero_angle/n_ticks; + avg_elec_angle += zero_angle/samples_per_full_rotation; #ifdef SIMPLEFOC_CALIBRATEDSENSOR_DEBUG SIMPLEFOC_DEBUG(">zero:",zero_angle); - SIMPLEFOC_DEBUG(">zero_average:", (float)(avg_elec_angle/2.0)); + SIMPLEFOC_DEBUG(">zero_average:", (float)(avg_elec_angle)); #endif } _delay(2000); @@ -206,13 +215,13 @@ void CalibratedSensor::calibrate(FOCMotor &motor, int settle_time_ms) /* backwards rotation */ - SIMPLEFOC_DEBUG(motor.sensor_direction == Direction::CCW ? "Rotating: CW" : "Rotating: CCW" ); - for (int i = n_ticks - 1; i >= 0; i--) + SIMPLEFOC_DEBUG(motor.sensor_direction == Direction::CCW ? "SEN_CAL: Rotating: CW" : "SEN_CAL: Rotating: CCW" ); + for (int i = samples_per_full_rotation - 1; i >= 0; i--) { - for (int j = 0; j < n2_ticks; j++) // move to the next location + for (int j = 0; j < intermeditate_positions; j++) // move to the next location { _wrapped.update(); - elec_angle -= deltaElectricalAngle; + elec_angle -= el_angle_increment; motor.setPhaseVoltage(voltage_calibration, 0, elec_angle); } @@ -221,9 +230,9 @@ void CalibratedSensor::calibrate(FOCMotor &motor, int settle_time_ms) _wrapped.update(); // calculate the error theta_actual = (int)motor.sensor_direction * (_wrapped.getAngle() - theta_init); - error[i] += 0.5 * (theta_actual - elec_angle / _NPP); + error[i] += 0.5 * (theta_actual - elec_angle / pole_pairs); // calculate the current electrical zero angle - float zero_angle = ((int)motor.sensor_direction * _wrapped.getMechanicalAngle() * _NPP ) - (elec_angle + _PI_2); + float zero_angle = ((int)motor.sensor_direction * _wrapped.getMechanicalAngle() * pole_pairs ) - (elec_angle + _PI_2); zero_angle = _normalizeAngle(zero_angle); // remove the 2PI jumps if(zero_angle - zero_angle_prev > _PI){ @@ -232,7 +241,7 @@ void CalibratedSensor::calibrate(FOCMotor &motor, int settle_time_ms) zero_angle = zero_angle + _2PI; } zero_angle_prev = zero_angle; - avg_elec_angle += zero_angle/n_ticks; + avg_elec_angle += zero_angle/samples_per_full_rotation; #ifdef SIMPLEFOC_CALIBRATEDSENSOR_DEBUG SIMPLEFOC_DEBUG(">zero:", zero_angle); SIMPLEFOC_DEBUG(">zero_average:", (float)(avg_elec_angle/2.0)); @@ -251,20 +260,25 @@ void CalibratedSensor::calibrate(FOCMotor &motor, int settle_time_ms) // calculating the average zero electrical angle from the forward calibration. motor.zero_electric_angle = _normalizeAngle(avg_elec_angle / (2.0)); - SIMPLEFOC_DEBUG("SEN_CAL:Average Zero Electrical Angle: ", motor.zero_electric_angle); + SIMPLEFOC_DEBUG("SEN_CAL: Average Zero Electrical Angle: ", motor.zero_electric_angle); _delay(1000); // Perform filtering to linearize position sensor eccentricity // FIR n-sample average, where n = number of samples in one electrical cycle // This filter has zero gain at electrical frequency and all integer multiples // So cogging effects should be completely filtered out + + // The fileter window size is set to samples_per_elec_rotation - one electrical cycle + // important for cogging filtering !!! + // window size for moving average filter of raw error + const int window_size = samples_per_elec_rotation; float error_mean = 0.0; - this->filter_error(error, error_mean, n_ticks, window); + this->filter_error(error, error_mean, samples_per_full_rotation, window_size); _delay(1000); // calculate offset index int index_offset = floor((float)n_lut * raw_offset / _2PI); - float dn = n_ticks / (float)n_lut; + float dn = samples_per_full_rotation / (float)n_lut; SIMPLEFOC_DEBUG("SEN_CAL: Constructing LUT."); _delay(1000); @@ -293,26 +307,29 @@ void CalibratedSensor::calibrate(FOCMotor &motor, int settle_time_ms) void CalibratedSensor::printLUT(FOCMotor &motor, Print &printer) { // Display the LUT - printer.print("uint16_t calibrationLut["); + printer.println(F("")); + printer.println(F("// Calibrated Sensor LUT")); + printer.print(F("uint16_t calibrationLut[")); printer.print(n_lut); - printer.println("] = {"); + printer.println(F("] = {")); _delay(100); for (int i=0;i < n_lut; i++){ printer.print(calibrationLut[i]); - if(i < n_lut - 1) printer.print(", "); + if(i < n_lut - 1) printer.print(F(", ")); _delay(1); } - printer.println(""); - printer.println("};"); + printer.println(F("")); + printer.println(F("};")); _delay(1000); // Display the zero electrical angle - printer.print("float zero_electric_angle = "); + printer.print(F("float zero_electric_angle = ")); printer.print(motor.zero_electric_angle); - printer.println(";"); + printer.println(F(";")); // Display the sensor direction - printer.print("Direction sensor_direction = "); + printer.print(F("Direction sensor_direction = ")); printer.println(motor.sensor_direction == Direction::CCW ? "Direction::CCW;" : "Direction::CW;"); + printer.println(F("")); _delay(1000); } \ No newline at end of file diff --git a/src/encoders/calibrated/CalibratedSensor.h b/src/encoders/calibrated/CalibratedSensor.h index d61977f..7fc588a 100644 --- a/src/encoders/calibrated/CalibratedSensor.h +++ b/src/encoders/calibrated/CalibratedSensor.h @@ -9,7 +9,10 @@ // LUT quantization constants for uint16_t encoding // Maps radians in range [-PI, PI] to uint16_t [0, 65535] -#define LUT_SCALE 10430.2191955f // 65535 / (2*PI) +// The real error is probably going to be much smaller than that +// range so in some cases it might make sense to +// raise the LUT_SCALE constants to improve resolution. +#define LUT_SCALE 10430.2191955f // 65535 / (2*PI) ~ #define LUT_OFFSET 32767.0f // center value for symmetric range class CalibratedSensor: public Sensor{ diff --git a/src/encoders/calibrated/README.md b/src/encoders/calibrated/README.md index b2e5198..11ec8cb 100644 --- a/src/encoders/calibrated/README.md +++ b/src/encoders/calibrated/README.md @@ -1,33 +1,43 @@ # Calibrated Sensor -by [@MarethyuPrefect](https://github.com/MarethyuPrefect) +Originally implemented by [@MarethyuPrefect](https://github.com/MarethyuPrefect) -A SimpleFOC Sensor wrapper implementation which adds sensor eccentricity calibration. +A SimpleFOC Sensor wrapper that corrects for **eccentricity errors** in magnetic sensor measurements. It builds a lookup table (LUT) during calibration to linearize sensor output and improve torque control accuracy. -Please also see our [forum thread](https://community.simplefoc.com/t/simplefoc-sensor-eccentricity-calibration/2212) on this topic. +**More info**: See the [SimpleFOC forum thread](https://community.simplefoc.com/t/simplefoc-sensor-eccentricity-calibration/2212) for detailed discussion. +## The Problem: Sensor Eccentricity -When you mount your (magnetic) sensor on your frame or motor, there will always be a slight misalignment between magnet and sensor (measurement system). This misalignment between center of rotation and the center of the sensor is called the eccentricity error. +When mounting a magnetic sensor on a motor, the magnet and sensor centers are rarely perfectly aligned. This **eccentricity error** causes: +- Non-linear sensor output across the rotor's range +- Variable measurement errors as the motor rotates +- Distortion in I_q (torque) control, reducing control performance -As a result your measurement system output is non-linear with respect to the rotor of the motor. This will cause an error with respect to the ideal torque you attempt to create with the I_q vector as function of the position. You could interpret this as a disturbance on your control loop which you want to minimize for optimal performance. +Example: A 0.5mm offset on a 3mm-diameter magnet can introduce ~10% error in angle measurement. -This calibration compensates the sensor reading in a feed forward fashion such that your performance improves. +## The Solution: Calibration LUT + +CalibratedSensor runs a calibration routine that: +1. Rotates the motor through several full turns +2. Records raw sensor readings at fixed intervals (creates lookup table) +3. Outputs corrective offset values (or stores them on disk) +4. Applies the LUT to sensor readings in real-time, linearizing output + +Result: Non-linearity is greatly reduced, improving torque control loop stability and accuracy. ## Hardware setup -Connect your sensor as usual. Make sure the sensor is working 'normally' i.e. without calibration first. Once things are working and tuned without sensor calibration, you can add the CalibratedSensor to see if you get an improvement. +Connect your sensor as usual. Make sure the sensor is working 'normally' i.e. without calibration first. Once things are working and tuned without sensor calibration, you can add the `CalibratedSensor` to see if you get an improvement. Note that during calibration, the motor is turned through several turns, and should be in an unloaded condition. Please ensure your hardware setup can support the motor rotating through full turns. ## Softwate setup -The CalibratedSensor acts as a wrapper to the actual sensor class. When creating the CalibratedSensor object, provide the real -sensor to the constructor of CalibratedSensor. +The `CalibratedSensor` acts as a wrapper to the actual `Sensor` class. When creating the `CalibratedSensor` object, provide the real sensor to the constructor of `CalibratedSensor`. -First, initialize the real sensor instance as normal. Then, call calibrate() on the CalibratedSensor instance. Then link the -CalibratedSensor to the motor and call motor.initFOC(). +First, initialize the real sensor instance as normal. Then, call `calibrate()` on the `CalibratedSensor` instance. Then link the `CalibratedSensor` to the motor and call `motor.initFOC()`. The motor will then use the calibrated sensor instance. @@ -42,6 +52,11 @@ BLDCDriver3PWM driver = BLDCDriver3PWM(PB4,PC7,PB10,PA9); CalibratedSensor sensor_calibrated = CalibratedSensor(sensor); void setup() { + // use monitoring with serial + Serial.begin(115200); + // enable debug output + SimpleFOCDebug::enable(&Serial); + sensor.init(); // Link motor to sensor motor.linkSensor(&sensor); @@ -49,18 +64,8 @@ void setup() { driver.voltage_power_supply = 20; driver.init(); motor.linkDriver(&driver); - // aligning voltage - motor.voltage_sensor_align = 8; - motor.voltage_limit = 20; - // set motion control loop to be used - motor.controller = MotionControlType::torque; - - // use monitoring with serial - Serial.begin(115200); // comment out if not needed motor.useMonitoring(Serial); - motor.monitor_variables = _MON_VEL; - motor.monitor_downsample = 10; // default 10 // initialize motor motor.init(); @@ -69,8 +74,6 @@ void setup() { sensor_calibrated.voltage_calibration = 6; // Running calibration sensor_calibrated.calibrate(motor); - - //Serial.println("Calibrating Sensor Done."); // Linking sensor to motor object motor.linkSensor(&sensor_calibrated); @@ -81,59 +84,71 @@ void setup() { Please see the more complete [example](https://github.com/simplefoc/Arduino-FOC-drivers/blob/master/examples/encoders/calibrated/sensor_calibration.ino) in our examples directory. -## EDIT March 2025 +## Saving the LUT persistently in the code +After running the calibration once, you can save the generated LUT and load it on startup to avoid recalibration on each startup. -The code has been rewritten to reduce its memory footprint and allow more flexible Lookup table (LUT) sizing. -Additionally, the calibrated sensor class now supports providing the saved LUT as a paramer for calibration. This allows you to save the LUT and load it on startup to avoid recalibration on each startup. +Make sure to call `calibrate()` once to generate the LUT, then use `printLUT()` to output the LUT to Serial. -Once you do the calibration once, it will output something like this: +```cpp +sensor_calibrated.calibrate(motor); // run the calibration +sensor_calibrated.printLUT(motor, Serial); // print the LUT to serial monitor +``` + +Copy the output and paste it into your code as shown below. ``` ... - -Starting Sensor Calibration. -MOT: Align sensor. +SEN_CAL: Starting Sensor Calibration. MOT: sensor_direction==CCW MOT: PP check: OK! -MOT: Zero elec. angle: 3.17 +MOT: Zero elec. angle: 2.83 MOT: No current sense. -MOT: Ready.Rotating: CCW -Rotating: CW -Average Zero Electrical Angle: 4.01 -Constructing LUT. - -uint16_t calibrationLut[50] = {32803, 32828, 32843, 32854, 32859, 32846, 32830, 32810, 32781, 32753, 32725, 32689, 32658, 32631, 32612, 32586, 32576, 32568, 32571, 32588, 32609, 32638, 32678, 32726, 32775, 32822, 32870, 32906, 32939, 32956, 32959, 32944, 32941, 32929, 32909, 32887, 32858, 32831, 32800, 32771, 32743, 32722, 32706, 32694, 32693, 32693, 32705, 32728, 32749, 32777}; -float zero_electric_angle = 4.007072; +MOT: Ready. +SEN_CAL: Rotating: CCW +SEN_CAL: Rotating: CW +SEN_CAL: Average Zero Electrical Angle: 2.74 +SEN_CAL: Constructing LUT. +SEN_CAL: Sensor Calibration Done. + +// Calibrated Sensor LUT +uint16_t calibrationLut[200] = {32701, 32694, 32689, 32684, 32678, 32671, 32665, 32661, 32656, 32649, 32644, 32641, 32638, 32634, 32629, 32624, 32619, 32612, 32609, 32607, 32604, 32600, 32596, 32594, 32594, 32591, 32588, 32587, 32588, 32592, 32593, 32594, 32594, 32594, 32597, 32602, 32605, 32609, 32614, 32620, 32627, 32632, 32636, 32642, 32649, 32658, 32666, 32673, 32681, 32687, 32692, 32702, 32711, 32719, 32725, 32732, 32740, 32748, 32755, 32760, 32766, 32773, 32781, 32787, 32792, 32796, 32797, 32800, 32804, 32807, 32809, 32809, 32810, 32810, 32812, 32810, 32808, 32808, 32808, 32808, 32807, 32804, 32804, 32803, 32802, 32803, 32805, 32805, 32804, 32803, 32803, 32805, 32804, 32802, 32803, 32805, 32820, 32824, 32826, 32823, 32817, 32810, 32807, 32807, 32807, 32805, 32803, 32803, 32806, 32809, 32808, 32807, 32806, 32804, 32802, 32799, 32796, 32791, 32791, 32791, 32789, 32787, 32786, 32786, 32785, 32783, 32781, 32783, 32784, 32786, 32788, 32790, 32790, 32791, 32791, 32795, 32800, 32803, 32806, 32810, 32815, 32820, 32824, 32827, 32832, 32838, 32845, 32850, 32856, 32860, 32863, 32868, 32874, 32879, 32883, 32886, 32890, 32894, 32899, 32902, 32903, 32906, 32911, 32912, 32911, 32909, 32905, 32903, 32902, 32899, 32895, 32891, 32886, 32883, 32879, 32872, 32866, 32860, 32856, 32850, 32843, 32835, 32827, 32818, 32810, 32804, 32798, 32790, 32781, 32774, 32768, 32761, 32753, 32745, 32739, 32735, 32729, 32722, 32716, 32708}; +float zero_electric_angle = 2.74; Direction sensor_direction = Direction::CCW; -Sensor Calibration Done ... ``` The LUT and sensor's zero angle and direction are outputed by the calibration process to the Serial terminal. So you can copy and paste them into your code. +## Workflow: Fast Recalibration Using Saved LUT + +If you calibrate once during setup and save the LUT to EEPROM or hardcode it, you can skip re-calibration on every startup: + +1. **First run**: Call `calibrate(motor)` → Serial outputs LUT +2. **Copy LUT**: Paste the generated values into your code +3. **Subsequent runs**: Pass LUT to constructor and `calibrate()` → instantaneous, no rotation needed + Your code will look something like this: ```c++ // number of LUT entries -const N_LUT = 50; +const N_LUT = 200; // Lookup table that has been output from the calibration process // The LUT is now stored as uint16_t for 50% memory savings (2 bytes vs 4 bytes per entry) -uint16_t calibrationLut[50] = {32803, 32828, 32843, 32854, 32859, 32846, 32830, 32810, 32781, 32753, 32725, 32689, 32658, 32631, 32612, 32586, 32576, 32568, 32571, 32588, 32609, 32638, 32678, 32726, 32775, 32822, 32870, 32906, 32939, 32956, 32959, 32944, 32941, 32929, 32909, 32887, 32858, 32831, 32800, 32771, 32743, 32722, 32706, 32694, 32693, 32693, 32705, 32728, 32749, 32777}; -float zero_electric_angle = 4.007072; +// Calibrated Sensor LUT +uint16_t calibrationLut[200] = {32701, 32694, 32689, 32684, 32678, 32671, 32665, 32661, 32656, 32649, 32644, 32641, 32638, 32634, 32629, 32624, 32619, 32612, 32609, 32607, 32604, 32600, 32596, 32594, 32594, 32591, 32588, 32587, 32588, 32592, 32593, 32594, 32594, 32594, 32597, 32602, 32605, 32609, 32614, 32620, 32627, 32632, 32636, 32642, 32649, 32658, 32666, 32673, 32681, 32687, 32692, 32702, 32711, 32719, 32725, 32732, 32740, 32748, 32755, 32760, 32766, 32773, 32781, 32787, 32792, 32796, 32797, 32800, 32804, 32807, 32809, 32809, 32810, 32810, 32812, 32810, 32808, 32808, 32808, 32808, 32807, 32804, 32804, 32803, 32802, 32803, 32805, 32805, 32804, 32803, 32803, 32805, 32804, 32802, 32803, 32805, 32820, 32824, 32826, 32823, 32817, 32810, 32807, 32807, 32807, 32805, 32803, 32803, 32806, 32809, 32808, 32807, 32806, 32804, 32802, 32799, 32796, 32791, 32791, 32791, 32789, 32787, 32786, 32786, 32785, 32783, 32781, 32783, 32784, 32786, 32788, 32790, 32790, 32791, 32791, 32795, 32800, 32803, 32806, 32810, 32815, 32820, 32824, 32827, 32832, 32838, 32845, 32850, 32856, 32860, 32863, 32868, 32874, 32879, 32883, 32886, 32890, 32894, 32899, 32902, 32903, 32906, 32911, 32912, 32911, 32909, 32905, 32903, 32902, 32899, 32895, 32891, 32886, 32883, 32879, 32872, 32866, 32860, 32856, 32850, 32843, 32835, 32827, 32818, 32810, 32804, 32798, 32790, 32781, 32774, 32768, 32761, 32753, 32745, 32739, 32735, 32729, 32722, 32716, 32708}; +float zero_electric_angle = 2.74; Direction sensor_direction = Direction::CCW; // provide the sensor class and the number of points in the LUT -CalibratedSensor sensor_calibrated = CalibratedSensor(sensor, N_LUT); +CalibratedSensor sensor_calibrated = CalibratedSensor(sensor, N_LUT, calibrationLut); ... void setup() { ... - // as LUT is provided to this function - sensor_calibrated.calibrate(motor, calibrationLut, zero_eletrical_angle, sensor_direction); - ... - + // No need to call calibrate() any more as the LUT is provided + // Only link the calibrated sensor to the motor motor.linkSensor(&sensor_calibrated); ... @@ -144,16 +159,17 @@ void setup() { ``` -## Implementation Details - -### Memory Optimization +## Changelog -The LUT is now stored using `uint16_t` instead of `float`, providing a 50% reduction in memory usage: -- **Old**: 4 bytes per entry (float) -- **New**: 2 bytes per entry (uint16_t) -- **Example**: For a 200-point LUT: 800 bytes → 400 bytes +#### **Dec 2024** +- A rewrite to reduce memory usage passed to `uint16_t` LUT type. + - Previous `float` LUT used 4 bytes per entry, `uint16_t` uses 2 bytes - a 50% reduction. + - The LUT values are scaled internally to maintain precision. + - Precision is 2PI/65536 radians (~0.005 degrees), which is sufficient for most applications. If more precision is needed, consider increasing the LUT_SCALE constant in `CalibratedSensor.h`. +- Fixed bug due to hardcoded number of sampled positions. + - Previously, the LUT size was variable but the number of sampled positions was hardcoded to `pole_pairs * 5`. + - Now the number of samples is derived from the LUT size provided to the constructor. -The quantization maps the offset range [-π, π] radians to [0, 65535], providing a resolution of approximately 0.0001 radians (0.0057°), which is more than sufficient for motor control applications. ## Future work From b4492480b6c120e34e249903d567acf18737280e Mon Sep 17 00:00:00 2001 From: gospar Date: Fri, 5 Dec 2025 11:01:17 +0100 Subject: [PATCH 3/3] update the examples --- .../calibrated/calibrated.ino | 3 +++ .../calibration_save/calibration_save.ino | 19 +++++++++++++------ src/encoders/calibrated/README.md | 3 +++ 3 files changed, 19 insertions(+), 6 deletions(-) diff --git a/examples/encoders/calibrated_sensor/calibrated/calibrated.ino b/examples/encoders/calibrated_sensor/calibrated/calibrated.ino index 6e4e303..16f6668 100644 --- a/examples/encoders/calibrated_sensor/calibrated/calibrated.ino +++ b/examples/encoders/calibrated_sensor/calibrated/calibrated.ino @@ -13,6 +13,7 @@ BLDCDriver3PWM driver = BLDCDriver3PWM(PB4,PC7,PB10,PA9); // instantiate the calibrated sensor object // argument 1 - sensor object // argument 2 - number of samples in the LUT (default 200) +// argument 3 - pointer to LUT array (if null, LUT will be filled only during calibration) CalibratedSensor sensor_calibrated = CalibratedSensor(sensor); // voltage set point variable @@ -57,6 +58,8 @@ void setup() { // Running calibration // it will ouptut the LUT and the zero electrical angle to the serial monitor !!!! sensor_calibrated.calibrate(motor); + // print the LUT to serial monitor + sensor_calibrated.printLUT(motor, Serial); //Serial.println("Calibrating Sensor Done."); // Linking sensor to motor object diff --git a/examples/encoders/calibrated_sensor/calibration_save/calibration_save.ino b/examples/encoders/calibrated_sensor/calibration_save/calibration_save.ino index da49b73..1798cc4 100644 --- a/examples/encoders/calibrated_sensor/calibration_save/calibration_save.ino +++ b/examples/encoders/calibrated_sensor/calibration_save/calibration_save.ino @@ -4,19 +4,18 @@ * * 1. Run this Sketch as is and wait for the calibration data to be generated and printed over Serial. * 2. Then copy the output from Serial to calibrationLut, zero_electric_angle and sensor_direction - * 3. Change values_provided to true and run the Sketch again to see the motor skipping the calibration. + * 3. Run the same example again and the code will use the provided calibration data and skip the calibration procedure. */ #include #include #include "encoders/calibrated/CalibratedSensor.h" -// fill this array with the calibration values outputed by the calibration procedure -float calibrationLut[50] = {0}; +// Replace these with the calibration data obtained from the first run +uint16_t calibrationLut[50] = {0}; float zero_electric_angle = 0; Direction sensor_direction = Direction::UNKNOWN; -const bool values_provided = false; // magnetic sensor instance - SPI MagneticSensorSPI sensor = MagneticSensorSPI(AS5147_SPI, 14); @@ -29,7 +28,10 @@ StepperDriver4PWM driver = StepperDriver4PWM(10, 9, 5, 6,8); // argument 2 - number of samples in the LUT (default 200) // argument 3 - pointer to the LUT array (defualt nullptr) CalibratedSensor sensor_calibrated = CalibratedSensor( - sensor, sizeof(calibrationLut) / sizeof(calibrationLut[0])); + sensor, + sizeof(calibrationLut)/sizeof(calibrationLut[0]), // number of samples + sensor_direction==Direction::UNKNOWN ? nullptr : calibrationLut // pointer to LUT array +); // voltage set point variable float target_voltage = 2; @@ -64,12 +66,17 @@ void setup() { // initialize motor motor.init(); - if(values_provided) { + if(sensor_direction != Direction::UNKNOWN){ + Serial.println(F("Using saved calibration data.")); + // provide the saved zero angle and direction motor.zero_electric_angle = zero_electric_angle; motor.sensor_direction = sensor_direction; } else { // Running calibration + Serial.println(F("Running calibration...")); sensor_calibrated.calibrate(motor); + // print the LUT to serial monitor + sensor_calibrated.printLUT(motor, Serial); } // Linking sensor to motor object diff --git a/src/encoders/calibrated/README.md b/src/encoders/calibrated/README.md index 11ec8cb..01d467a 100644 --- a/src/encoders/calibrated/README.md +++ b/src/encoders/calibrated/README.md @@ -150,6 +150,9 @@ void setup() { // No need to call calibrate() any more as the LUT is provided // Only link the calibrated sensor to the motor motor.linkSensor(&sensor_calibrated); + // provide the saved zero angle and direction + motor.zero_electric_angle = zero_electric_angle; + motor.sensor_direction = sensor_direction; ... motor.initFOC();