LCOV - code coverage report
Current view: top level - AP_AccelCal - AccelCalibrator.cpp (source / functions) Hit Total Coverage
Test: lcov.info Lines: 184 231 79.7 %
Date: 2026-09-09 11:02:08 Functions: 19 21 90.5 %

          Line data    Source code
       1             : /*
       2             :    This program is free software: you can redistribute it and/or modify
       3             :    it under the terms of the GNU General Public License as published by
       4             :    the Free Software Foundation, either version 3 of the License, or
       5             :    (at your option) any later version.
       6             :    This program is distributed in the hope that it will be useful,
       7             :    but WITHOUT ANY WARRANTY; without even the implied warranty of
       8             :    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
       9             :    GNU General Public License for more details.
      10             :    You should have received a copy of the GNU General Public License
      11             :    along with this program.  If not, see <http://www.gnu.org/licenses/>.
      12             : */
      13             : 
      14             : // Authored by Jonathan Challinger, 3D Robotics Inc.
      15             : 
      16             : #include "AccelCalibrator.h"
      17             : #include <stdio.h>
      18             : #include <AP_HAL/AP_HAL.h>
      19             : 
      20             : const extern AP_HAL::HAL& hal;
      21             : /*
      22             :  * TODO
      23             :  * - time out when not receiving samples
      24             :  */
      25             : 
      26             : ////////////////////////////////////////////////////////////
      27             : ///////////////////// PUBLIC INTERFACE /////////////////////
      28             : ////////////////////////////////////////////////////////////
      29             : 
      30           7 : AccelCalibrator::AccelCalibrator() :
      31           7 : _conf_tolerance(ACCEL_CAL_TOLERANCE),
      32           7 : _sample_buffer(nullptr)
      33             : {
      34           7 :     clear();
      35           7 : }
      36             : /*
      37             :     Select options, initialise variables and initiate accel calibration
      38             :     Options:
      39             :     Fit Type:       Will assume that if accelerometer static samples around all possible orientation
      40             :                     are spread in space will cover a surface of AXIS_ALIGNED_ELLIPSOID or any general 
      41             :                     ELLIPSOID as chosen
      42             : 
      43             :     Num Samples:    Number of samples user should will be gathering, please note that with type of surface
      44             :                     chosen the minimum number of samples required will vary, for instance in the case of AXIS
      45             :                     ALIGNED ELLIPSOID atleast 6 distinct samples are required while for generic ELLIPSOIDAL fit
      46             :                     which will include calculation of offdiagonal parameters too requires atleast 8 parameters
      47             :                     to form a distinct ELLIPSOID
      48             : 
      49             :     Sample Time:    Time over which the samples will be gathered and averaged to add to a single sample for least
      50             :                     square regression
      51             : 
      52             :     offset,diag,offdiag: initial parameter values for LSQ calculation
      53             : */
      54           4 : void AccelCalibrator::start(enum accel_cal_fit_type_t fit_type, uint8_t num_samples, float sample_time) {
      55           4 :     start(fit_type, num_samples, sample_time, Vector3f(0,0,0), Vector3f(1,1,1), Vector3f(0,0,0));
      56           4 : }
      57             : 
      58           4 : void AccelCalibrator::start(enum accel_cal_fit_type_t fit_type, uint8_t num_samples, float sample_time, Vector3f offset, Vector3f diag, Vector3f offdiag) {
      59           4 :     if (_status == ACCEL_CAL_FAILED || _status == ACCEL_CAL_SUCCESS) {
      60           0 :         clear();
      61             :     }
      62           4 :     if (_status != ACCEL_CAL_NOT_STARTED) {
      63           0 :         return;
      64             :     }
      65             : 
      66           4 :     _conf_num_samples = num_samples;
      67           4 :     _conf_sample_time = sample_time;
      68           4 :     _conf_fit_type = fit_type;
      69             : 
      70           4 :     const uint16_t faces = 2*_conf_num_samples-4;
      71           4 :     const float a = (4.0f * M_PI / (3.0f * faces)) + M_PI / 3.0f;
      72           4 :     const float theta = 0.5f * acosf(cosf(a) / (1.0f - cosf(a)));
      73           4 :     _min_sample_dist = GRAVITY_MSS * 2*sinf(theta/2);
      74             : 
      75           4 :     _param.s.offset = offset;
      76           4 :     _param.s.diag = diag;
      77           4 :     _param.s.offdiag = offdiag;
      78             : 
      79           4 :     switch (_conf_fit_type) {
      80           4 :         case ACCEL_CAL_AXIS_ALIGNED_ELLIPSOID:
      81           4 :             if (_conf_num_samples < 6) {
      82           0 :                 set_status(ACCEL_CAL_FAILED);
      83           0 :                 return;
      84             :             }
      85           4 :             break;
      86           0 :         case ACCEL_CAL_ELLIPSOID:
      87           0 :             if (_conf_num_samples < 8) {
      88           0 :                 set_status(ACCEL_CAL_FAILED);
      89           0 :                 return;
      90             :             }
      91           0 :             break;
      92             :     }
      93             : 
      94           4 :     set_status(ACCEL_CAL_WAITING_FOR_ORIENTATION);
      95             : }
      96             : 
      97             : 
      98             : // set Accel calibrator status to make itself ready for future accel cals
      99          13 : void AccelCalibrator::clear() {
     100          13 :     set_status(ACCEL_CAL_NOT_STARTED);
     101          13 : }
     102             : 
     103             : // returns true if accel calibrator is running
     104          14 : bool AccelCalibrator::running() {
     105          14 :     return _status == ACCEL_CAL_WAITING_FOR_ORIENTATION || _status == ACCEL_CAL_COLLECTING_SAMPLE;
     106             : }
     107             : 
     108             : // set Accel calibrator to start collecting samples in the next cycle
     109          12 : void AccelCalibrator::collect_sample() {
     110          12 :     set_status(ACCEL_CAL_COLLECTING_SAMPLE);
     111          12 : }
     112             : 
     113             : // collect and avg sample to be passed onto LSQ estimator after all requisite orientations are done
     114         311 : void AccelCalibrator::new_sample(const Vector3f& delta_velocity, float dt) {
     115         311 :     if (_status != ACCEL_CAL_COLLECTING_SAMPLE) {
     116           0 :         return;
     117             :     }
     118             : 
     119         311 :     if (_samples_collected >= _conf_num_samples) {
     120           0 :         set_status(ACCEL_CAL_FAILED);
     121           0 :         return;
     122             :     }
     123             : 
     124         311 :     _sample_buffer[_samples_collected].delta_velocity += delta_velocity;
     125         311 :     _sample_buffer[_samples_collected].delta_time += dt;
     126             : 
     127         311 :     _last_samp_frag_collected_ms = AP_HAL::millis();
     128             : 
     129         311 :     if (_sample_buffer[_samples_collected].delta_time > _conf_sample_time) {
     130          12 :         Vector3f sample = _sample_buffer[_samples_collected].delta_velocity/_sample_buffer[_samples_collected].delta_time;
     131          12 :         if (!accept_sample(sample)) {
     132           0 :             set_status(ACCEL_CAL_FAILED);
     133           0 :             return;
     134             :         }
     135             : 
     136          12 :         _samples_collected++;
     137             : 
     138          12 :         if (_samples_collected >= _conf_num_samples) {
     139           2 :             run_fit(MAX_ITERATIONS, _fitness);
     140             : 
     141           2 :             if (_fitness < _conf_tolerance && accept_result()) {
     142           2 :                 set_status(ACCEL_CAL_SUCCESS);
     143             :             } else {
     144           0 :                 set_status(ACCEL_CAL_FAILED);
     145             :             }
     146             :         } else {
     147          10 :             set_status(ACCEL_CAL_WAITING_FOR_ORIENTATION);
     148             :         }
     149             :     }
     150             : }
     151             : 
     152             : // determines if the result is acceptable
     153           2 : bool AccelCalibrator::accept_result() const {
     154           2 :     if (fabsf(_param.s.offset.x) > GRAVITY_MSS ||
     155           2 :         fabsf(_param.s.offset.y) > GRAVITY_MSS ||
     156           2 :         fabsf(_param.s.offset.z) > GRAVITY_MSS ||
     157           2 :         _param.s.diag.x < 0.8f || _param.s.diag.x > 1.2f ||
     158           2 :         _param.s.diag.y < 0.8f || _param.s.diag.y > 1.2f ||
     159           2 :         _param.s.diag.z < 0.8f || _param.s.diag.z > 1.2f) {
     160           0 :         return false;
     161             :     } else {
     162           2 :         return true;
     163             :     }
     164             : }
     165             : 
     166             : // interface for LSq estimator to read sample buffer sent after conversion from delta velocity
     167             : // to averaged acc over time
     168        1244 : bool AccelCalibrator::get_sample(uint8_t i, Vector3f& s) const {
     169        1244 :     if (i >= _samples_collected) {
     170           0 :         return false;
     171             :     }
     172        1244 :     s = _sample_buffer[i].delta_velocity / _sample_buffer[i].delta_time;
     173        1244 :     return true;
     174             : }
     175             : 
     176             : // returns true and sample corrected with diag offdiag parameters as calculated by LSq estimation procedure
     177             : // returns false if no correct parameter exists to be applied along with existing sample without corrections
     178           2 : bool AccelCalibrator::get_sample_corrected(uint8_t i, Vector3f& s) const {
     179           2 :     if (_status != ACCEL_CAL_SUCCESS || !get_sample(i,s)) {
     180           0 :         return false;
     181             :     }
     182             : 
     183             :     Matrix3f M (
     184           2 :         _param.s.diag.x    , _param.s.offdiag.x , _param.s.offdiag.y,
     185           2 :         _param.s.offdiag.x , _param.s.diag.y    , _param.s.offdiag.z,
     186           2 :         _param.s.offdiag.y , _param.s.offdiag.z , _param.s.diag.z
     187           2 :     );
     188             : 
     189           2 :     s = M*(s+_param.s.offset);
     190             : 
     191           2 :     return true;
     192             : }
     193             : 
     194             : // checks if no new sample has been received for considerable amount of time
     195          72 : void AccelCalibrator::check_for_timeout() {
     196          72 :     const uint32_t timeout = _conf_sample_time*2*1000 + 500;
     197          72 :     if (_status == ACCEL_CAL_COLLECTING_SAMPLE && AP_HAL::millis() - _last_samp_frag_collected_ms > timeout) {
     198           0 :         set_status(ACCEL_CAL_FAILED);
     199             :     }
     200          72 : }
     201             : 
     202             : // returns spherical fit parameters
     203           0 : void AccelCalibrator::get_calibration(Vector3f& offset) const {
     204           0 :     offset = -_param.s.offset;
     205           0 : }
     206             : 
     207             : // returns axis aligned ellipsoidal fit parameters
     208           2 : void AccelCalibrator::get_calibration(Vector3f& offset, Vector3f& diag) const {
     209           2 :     offset = -_param.s.offset;
     210           2 :     diag = _param.s.diag;
     211           2 : }
     212             : 
     213             : // returns generic ellipsoidal fit parameters
     214           0 : void AccelCalibrator::get_calibration(Vector3f& offset, Vector3f& diag, Vector3f& offdiag) const {
     215           0 :     offset = -_param.s.offset;
     216           0 :     diag = _param.s.diag;
     217           0 :     offdiag = _param.s.offdiag;
     218           0 : }
     219             : 
     220             : /////////////////////////////////////////////////////////////
     221             : ////////////////////// PRIVATE METHODS //////////////////////
     222             : /////////////////////////////////////////////////////////////
     223             : 
     224             : /*
     225             :   The sample acceptance distance is determined as follows:
     226             :   For any regular polyhedron with triangular faces, the angle theta subtended
     227             :   by two closest points is defined as
     228             :  
     229             :        theta = arccos(cos(A)/(1-cos(A)))
     230             :  
     231             :   Where:
     232             :        A = (4pi/F + pi)/3
     233             :   and
     234             :        F = 2V - 4 is the number of faces for the polyhedron in consideration,
     235             :        which depends on the number of vertices V
     236             :  
     237             :   The above equation was proved after solving for spherical triangular excess
     238             :   and related equations.
     239             :  */
     240          12 : bool AccelCalibrator::accept_sample(const Vector3f& sample)
     241             : {
     242          12 :     if (_sample_buffer == nullptr) {
     243           0 :         return false;
     244             :     }
     245             : 
     246          42 :     for(uint8_t i=0; i < _samples_collected; i++) {
     247          30 :         Vector3f other_sample;
     248          30 :         get_sample(i, other_sample);
     249             : 
     250          30 :         if ((other_sample - sample).length() < _min_sample_dist){
     251           0 :             return false;
     252             :         }
     253             :     }
     254          12 :     return true;
     255             : }
     256             : 
     257             : // sets status of calibrator and takes appropriate actions
     258          41 : void AccelCalibrator::set_status(enum accel_cal_status_t status) {
     259          41 :     switch (status) {
     260          13 :         case ACCEL_CAL_NOT_STARTED:                 
     261             :             //Calibrator not started
     262          13 :             _status = ACCEL_CAL_NOT_STARTED;
     263             : 
     264          13 :             _samples_collected = 0;
     265          13 :             if (_sample_buffer != nullptr) {
     266           2 :                 free(_sample_buffer);
     267           2 :                 _sample_buffer = nullptr;
     268             :             }
     269             : 
     270          13 :             break;
     271             : 
     272          14 :         case ACCEL_CAL_WAITING_FOR_ORIENTATION:     
     273             :             //Callibrator has been started and is waiting for user to ack after orientation setting
     274          14 :             if (!running()) {
     275           4 :                 _samples_collected = 0;
     276           4 :                 if (_sample_buffer == nullptr) {
     277           4 :                     _sample_buffer = (struct AccelSample*)calloc(_conf_num_samples,sizeof(struct AccelSample));
     278           4 :                     if (_sample_buffer == nullptr) {
     279           0 :                         set_status(ACCEL_CAL_FAILED);
     280           0 :                         break;
     281             :                     }
     282             :                 }
     283             :             }
     284          14 :             if (_samples_collected >= _conf_num_samples) {
     285           0 :                 break;
     286             :             }
     287          14 :             _status = ACCEL_CAL_WAITING_FOR_ORIENTATION;
     288          14 :             break;
     289             : 
     290          12 :         case ACCEL_CAL_COLLECTING_SAMPLE:
     291             :             // Calibrator is waiting on collecting samples from accelerometer for amount of 
     292             :             // time as requested by user/GCS
     293          12 :             if (_status != ACCEL_CAL_WAITING_FOR_ORIENTATION) {
     294           0 :                 break;
     295             :             }
     296          12 :             _last_samp_frag_collected_ms = AP_HAL::millis();
     297          12 :             _status = ACCEL_CAL_COLLECTING_SAMPLE;
     298          12 :             break;
     299             : 
     300           2 :         case ACCEL_CAL_SUCCESS:
     301             :             // Calibrator has successfully fitted the samples to user requested surface model 
     302             :             // and has passed tolerance test
     303           2 :             if (_status != ACCEL_CAL_COLLECTING_SAMPLE) {
     304           0 :                 break;
     305             :             }
     306             : 
     307           2 :             _status = ACCEL_CAL_SUCCESS;
     308           2 :             break;
     309             : 
     310           0 :         case ACCEL_CAL_FAILED:
     311             :             // Calibration has failed with reasons that can range from 
     312             :             // bad sample data leading to failure in tolerance test to lack of distinct samples
     313           0 :             if (_status == ACCEL_CAL_NOT_STARTED) {
     314           0 :                 break;
     315             :             }
     316             : 
     317           0 :             _status = ACCEL_CAL_FAILED;
     318           0 :             break;
     319             :     };
     320          41 : }
     321             : 
     322             : /*
     323             :     Run Gauss Newton fitting algorithm over the sample space and come up with offsets, diagonal/scale factors
     324             :     and crosstalk/offdiagonal parameters
     325             : */
     326           2 : void AccelCalibrator::run_fit(uint8_t max_iterations, float& fitness)
     327             : {
     328           2 :     if (_sample_buffer == nullptr) {
     329           0 :         return;
     330             :     }
     331           2 :     fitness = calc_mean_squared_residuals(_param.s);
     332           2 :     float min_fitness = fitness;
     333           2 :     union param_u fit_param = _param;
     334           2 :     uint8_t num_iterations = 0;
     335             : 
     336         102 :     while(num_iterations < max_iterations) {
     337         100 :         float JTJ[ACCEL_CAL_MAX_NUM_PARAMS*ACCEL_CAL_MAX_NUM_PARAMS] {};
     338         100 :         VectorP JTFI;
     339             : 
     340         700 :         for(uint16_t k = 0; k<_samples_collected; k++) {
     341         600 :             Vector3f sample;
     342         600 :             get_sample(k, sample);
     343             : 
     344         600 :             VectorN<float,ACCEL_CAL_MAX_NUM_PARAMS> jacob;
     345             : 
     346         600 :             calc_jacob(sample, fit_param.s, jacob);
     347             : 
     348        4200 :             for(uint8_t i = 0; i < get_num_params(); i++) {
     349             :                 // compute JTJ
     350       25200 :                 for(uint8_t j = 0; j < get_num_params(); j++) {
     351       21600 :                     JTJ[i*get_num_params()+j] += jacob[i] * jacob[j];
     352             :                 }
     353             :                 // compute JTFI
     354        3600 :                 JTFI[i] += jacob[i] * calc_residual(sample, fit_param.s);
     355             :             }
     356             :         }
     357             : 
     358         100 :         if (!mat_inverse(JTJ, JTJ, get_num_params())) {
     359           0 :             return;
     360             :         }
     361             : 
     362         700 :         for(uint8_t row=0; row < get_num_params(); row++) {
     363        4200 :             for(uint8_t col=0; col < get_num_params(); col++) {
     364        3600 :                 fit_param.a[row] -= JTFI[col] * JTJ[row*get_num_params()+col];
     365             :             }
     366             :         }
     367             : 
     368         100 :         fitness = calc_mean_squared_residuals(fit_param.s);
     369             : 
     370         100 :         if (isnan(fitness) || isinf(fitness)) {
     371           0 :             return;
     372             :         }
     373             : 
     374         100 :         if (fitness < min_fitness) {
     375          10 :             min_fitness = fitness;
     376          10 :             _param = fit_param;
     377             :         }
     378             : 
     379         100 :         num_iterations++;
     380             :     }
     381             : }
     382             : 
     383             : // calculates residual from samples(corrected using supplied parameter) to gravity
     384             : // used to create Fitness column matrix
     385        4212 : float AccelCalibrator::calc_residual(const Vector3f& sample, const struct param_t& params) const {
     386             :     Matrix3f M (
     387        4212 :         params.diag.x    , params.offdiag.x , params.offdiag.y,
     388        4212 :         params.offdiag.x , params.diag.y    , params.offdiag.z,
     389        4212 :         params.offdiag.y , params.offdiag.z , params.diag.z
     390        4212 :     );
     391        4212 :     return GRAVITY_MSS - (M*(sample+params.offset)).length();
     392             : }
     393             : 
     394             : // calculated the total mean squared fitness of all the collected samples using parameters
     395             : // supplied
     396         102 : float AccelCalibrator::calc_mean_squared_residuals(const struct param_t& params) const
     397             : {
     398         102 :     if (_sample_buffer == nullptr || _samples_collected == 0) {
     399           0 :         return 1.0e30f;
     400             :     }
     401         102 :     float sum = 0.0f;
     402         714 :     for(uint16_t i=0; i < _samples_collected; i++){
     403         612 :         Vector3f sample;
     404         612 :         get_sample(i, sample);
     405         612 :         float resid = calc_residual(sample, params);
     406         612 :         sum += sq(resid);
     407             :     }
     408         102 :     sum /= _samples_collected;
     409         102 :     return sum;
     410             : }
     411             : 
     412             : // calculate jacobian, a matrix that defines relation to variation in fitness with variation in each of the parameters
     413             : // this is used in LSq estimator to adjust variation in parameter to be used for next iteration of LSq
     414         600 : void AccelCalibrator::calc_jacob(const Vector3f& sample, const struct param_t& params, VectorP &ret) const {
     415         600 :     switch (_conf_fit_type) {
     416         600 :         case ACCEL_CAL_AXIS_ALIGNED_ELLIPSOID:
     417             :         case ACCEL_CAL_ELLIPSOID: {
     418         600 :             const Vector3f &offset = params.offset;
     419         600 :             const Vector3f &diag = params.diag;
     420         600 :             const Vector3f &offdiag = params.offdiag;
     421             :             Matrix3f M(
     422         600 :                 diag.x    , offdiag.x , offdiag.y,
     423         600 :                 offdiag.x , diag.y    , offdiag.z,
     424         600 :                 offdiag.y , offdiag.z , diag.z
     425         600 :             );
     426             : 
     427         600 :             float A =  (diag.x    * (sample.x + offset.x)) + (offdiag.x * (sample.y + offset.y)) + (offdiag.y * (sample.z + offset.z));
     428         600 :             float B =  (offdiag.x * (sample.x + offset.x)) + (diag.y    * (sample.y + offset.y)) + (offdiag.z * (sample.z + offset.z));
     429         600 :             float C =  (offdiag.y * (sample.x + offset.x)) + (offdiag.z * (sample.y + offset.y)) + (diag.z    * (sample.z + offset.z));
     430         600 :             float length = (M*(sample+offset)).length();
     431             : 
     432             :             // 0-2: offsets
     433         600 :             ret[0] = -1.0f * (((diag.x    * A) + (offdiag.x * B) + (offdiag.y * C))/length);
     434         600 :             ret[1] = -1.0f * (((offdiag.x * A) + (diag.y    * B) + (offdiag.z * C))/length);
     435         600 :             ret[2] = -1.0f * (((offdiag.y * A) + (offdiag.z * B) + (diag.z    * C))/length);
     436             :             // 3-5: diagonals
     437         600 :             ret[3] = -1.0f * ((sample.x + offset.x) * A)/length;
     438         600 :             ret[4] = -1.0f * ((sample.y + offset.y) * B)/length;
     439         600 :             ret[5] = -1.0f * ((sample.z + offset.z) * C)/length;
     440             :             // 6-8: off-diagonals
     441         600 :             ret[6] = -1.0f * (((sample.y + offset.y) * A) + ((sample.x + offset.x) * B))/length;
     442         600 :             ret[7] = -1.0f * (((sample.z + offset.z) * A) + ((sample.x + offset.x) * C))/length;
     443         600 :             ret[8] = -1.0f * (((sample.z + offset.z) * B) + ((sample.y + offset.y) * C))/length;
     444         600 :             return;
     445             :         }
     446             :     }
     447             : }
     448             : 
     449             : // returns number of parameters are required for selected Fit type
     450       59600 : uint8_t AccelCalibrator::get_num_params() const {
     451       59600 :     switch (_conf_fit_type) {
     452       59600 :         case ACCEL_CAL_AXIS_ALIGNED_ELLIPSOID:
     453       59600 :             return 6;
     454           0 :         case ACCEL_CAL_ELLIPSOID:
     455           0 :             return 9;
     456           0 :         default:
     457           0 :             return 6;
     458             :     }
     459             : }

Generated by: LCOV version 1.14