42 properties (Access =
private)
43 %
default settings from JUMPconnect
44 accRange = 16 * 9.81; %>
default acceleration range (m/s^2)
45 gyroRange = 2000; %>
default gyroscope range (°/s)
46 sampleRate = 200; %>
default sample rate of the MPU (Hz)
47 sampleRatePressure = 5; %> default sample rate of the pressure and temperature sensor (Hz)
48 sampleRateMagnetometer = 10; %> default sample rate of the magnetometer (Hz)
51 mpu {mustBeA(mpu,
'imuSensor')} = imuSensor; %> model of the MPU9250 9-axis IMU
52 hig {mustBeA(hig,
'imuSensor')} = imuSensor; %> model of the ADXL375 3-axis accelerometer
54 %> @note the barometer is implemented directly in the generate function
58 methods (Access = public)
60 % ====================================================================== %
61 %> @brief
JumpSensor is the constructor of the JUMP sensor.
63 %>
JumpSensor creates a JUMP sensor with the given settings. The
64 %> settings are the same as in the JUMPconnect software. The JUMP sensor
65 %> can be used to generate sensor data from true data.
67 %> @param accRange (optional) - the acceleration range of the MPU9250 in g
68 %> @param gyroRange (optional) - the gyroscope range of the MPU9250 in °/s
69 %> @param sampleRate (optional) - the sample rate of the MPU9250 in Hz
70 %> @param sampleRatePressure (optional) - the sample rate of the pressure and temperature sensor in Hz
72 %> @retval obj - the created JUMP sensor
73 % ====================================================================== %
76 args.accRange (1,1)
double {mustBeMember(args.accRange, [2,4,8,16])} = 16;
77 args.
gyroRange (1,1)
double {mustBeMember(args.gyroRange, [250,500,1000,2000])} = 2000;
78 args.sampleRate (1,1)
double {mustBeMember(args.sampleRate, [50,200])} = 200;
79 args.sampleRatePressure (1,1)
double {mustBeMember(args.sampleRatePressure, [1,5])} = 5;
82 obj.accRange = args.accRange * 9.81; % convert to m/s^2
83 obj.gyroRange = args.gyroRange;
84 obj.sampleRate = args.sampleRate;
85 obj.sampleRatePressure = args.sampleRatePressure;
87 % make checks on the settings
88 if obj.gyroRange ~= 2000
89 warning(
"The JUMPconnect software only allows the setting 2000 °/s.");
91 if (obj.sampleRate == 50 && obj.sampleRatePressure == 5) || (obj.sampleRate == 200 && obj.sampleRatePressure == 1)
92 warning(
"The JUMPconnect software does not allow the combination of %i Hz sample rate and %i Hz pressure sensor sample rate.", obj.sampleRate, obj.sampleRatePressure);
102 % ====================================================================== %
103 %> @brief generate generates sensor data from
true data.
105 %> generate generates sensor data from
true data with the defined sensor
106 %> models. The data format is identical to the data format of the parsing
107 %> function of the function JUMPREADDATA.
109 %> @param acc_true (nx3
double) - the true accelerometer data in m/s^2
110 %> @param gyr_true (nx3 double) - the true gyroscope data in deg/s
111 %> @param ori_true (nx1 quaternion) - the true orientation data in quaternions
112 %> @param options.alt_true (nx1 double, optional) - the true altitude in m (default: 500 m)
113 %> @param options.tmp_true (nx1
double, optional) - the true temperature in °C (default: 25°C)
114 %> @param options.t_start (datetime, optional) - the start time of the sensor data (default: now)
115 %> @param options.simulate_mpu (logical, optional) - simulate the MPU9250 sensor (default: true)
116 %> @param options.simulate_hig (logical, optional) - simulate the ADXL375 sensor (default: true)
117 %> @param options.simulate_prs (logical, optional) - simulate the MS5611 sensor (default: true)
118 %> @param options.simulate_tmp (logical, optional) - simulate the temperature sensor (default: true)
120 %> @retval data (struct) - the sensor data in the format of the parsing function of the function JUMPREADDATA
121 %> - data.tt (timetable) - timetable containing the sensor data
122 %> - data.ID (
char) - sensor ID
123 %> - data.UID (
char) - sensor UID
124 %> - data.name (
char) - sensor name (fixed to 'JUMP model')
125 %> - data.sw_version (
char) - sensor software version
126 %> - data.hw_version (
char) - sensor hardware version
127 %> - data.acc_range (
double) - accelerometer range
128 %> - data.gyro_range (
double) - gyroscope range
129 %> - data.mpu_cal (
double) - mpu calibration
130 %> - data.act_thrs (
double) - activity threshold
131 %> - data.inact_dur (
double) - inactivity duration
132 %> - data.mpu_rate (
double) - mpu rate
133 %> - data.adxl_rate (
double) - adxl rate
134 %> - data.pres_rate (
double) - pressure rate
135 %> - data.magn_en (
double) - magnetometer enabled
136 %> - data.sync (
double) - sync mode
137 %> - data.ble_adress (
char) - ble adress
139 %> @note for whatever reason the MATLAB imuSensor class negates the
140 %> accelerometer data. I guess, because they missunderstand how the
141 %> gravitational force works. According to their algorithm description,
142 %> they negate the input acceleration and then add the gravity vector.
143 %> This is not the correct way to do it. The correct way is to negate the
144 %> gravity vector. Therefore, the input is negated and the gravity vector
145 %> is added two times before the imuSensor class is called. This is
146 %> a bug in the MATLAB imuSensor class. In ither words, we have the
147 %> actual acceleration \f$ a \f$. to patch the MATLAB bevavior, we do:
148 %> \f[ a_{patched} = -a + 2 \cdot [0,0,g], \f] before we call the
149 %> imuSensor class.then the imuSensor class does
150 %> \f[ totalAcc = -a_{input} + [0,0, g] = -(-a + 2 \cdot [0,0,g]) +
151 %> [0,0, +g] = a - [0,0,g], \f] which brings the correct result.
153 %> @todo check the covariances of the sensors
155 %> @todo check the axis misalignment and sensitivity over time.
156 % ====================================================================== %
157 function data = generate(obj, acc_true, gyr_true, ori_true, options)
159 % check input arguments
162 acc_true (:,3) double;
163 gyr_true (:,3) double;
164 ori_true (:,1) quaternion;
165 options.alt_true (:,1) double = repmat(500, length(acc_true), 1);
166 options.tmp_true (:,1) double = repmat(25, length(acc_true), 1);
167 options.t_start (1,1) datetime = datetime(
"now");
168 options.simulate_mpu (1,1) logical =
true;
169 options.simulate_hig (1,1) logical =
true;
170 options.simulate_prs (1,1) logical =
true;
171 options.simulate_tmp (1,1) logical =
true;
175 alt_true = options.alt_true;
176 tmp_true = options.tmp_true;
177 t_start = options.t_start;
179 % check
if the input vectors have the same length
180 input_length = length(acc_true);
181 assert(length(gyr_true) == input_length,
"The input vectors must have the same length.");
182 assert(length(ori_true) == input_length,
"The input vectors must have the same length.");
183 assert(length(tmp_true) == input_length,
"The input vectors must have the same length.");
185 % modify the acceleration because of a MATLAB bug
186 acc_true = -acc_true + 2*[0,0,9.81]; % negate the acceleration and add 2x the gravity vector
188 if options.simulate_mpu
190 % generate the sensor data
191 gyr_true = gyr_true * pi / 180; % convert to rad/s
194 [acc_mpu, gyr_mpu, mag_mpu] = obj.mpu(acc_true, gyr_true, ori_true);
195 gyr_mpu = gyr_mpu * 180 / pi; % convert to deg/s
197 % nownsample and then upsample with previous value to get the same output as the real sensor
198 mag_mpu_ds = downsample(mag_mpu, obj.sampleRate / obj.sampleRateMagnetometer); % NOTE: the magnetometer is sampled at 10 Hz
199 mag_mpu = nan(input_length,3);
200 mag_mpu(1:obj.sampleRate/obj.sampleRateMagnetometer:end,:) = mag_mpu_ds;
201 mag_mpu = fillmissing(mag_mpu,
'previous');
204 acc_mpu = nan(input_length,3);
205 gyr_mpu = nan(input_length,3);
206 mag_mpu = nan(input_length,3);
209 if options.simulate_hig
211 acc_adxl = obj.hig(acc_true, zeros(input_length,3), ori_true);
213 acc_adxl = nan(input_length,3);
216 if options.simulate_prs
218 prs = obj.getPressureFromAltitude(alt_true, tmp_true);
220 prs = repmat(1013.25, input_length, 1);
224 if options.simulate_tmp
226 tmp = obj.getSensorTemperature(tmp_true);
232 t = repmat(t_start, input_length, 1) + ((0:input_length-1) .* seconds(1/obj.sampleRate))';
233 data.tt = timetable(t, acc_mpu, gyr_mpu, acc_adxl, mag_mpu, prs, tmp);
234 data.tt.Properties.VariableNames = {
'acc',
'gyr',
'hig',
'mag',
'prs',
'tmp'};
236 % add sensor information
237 data.t_start = t_start;
238 data.ID =
'S-24-A-SIMUL';
239 data.UID =
'simulation';
240 data.name =
'JUMP simulation';
241 data.sw_version =
'132000(s)';
242 data.hw_version =
'1627390748(Ã )';
243 data.acc_range = round(obj.accRange / 9.81); % convert to g
244 data.gyro_range = round(obj.gyroRange * 180 / pi); % convert to deg/s
248 data.mpu_rate = obj.sampleRate;
249 data.adxl_rate = obj.sampleRate;
250 data.pres_rate = obj.sampleRatePressure;
253 data.ble_address =
'001122334455';
257 % ====================================================================== %
258 %> @brief generates data from a @ref
Trajectory object.
260 %> generateFromTrajectory generates sensor data from a @ref
Trajectory
261 %>
object. The sensor data is generated with the defined sensor models.
264 %> @param traj (
Trajectory) - the trajectory object
266 %> @retval data (struct) - the sensor data in the format defined in @ref generate
267 % ====================================================================== %
268 function data = generateFromTrajectory(obj, traj)
270 % check input arguments
276 traj = traj.getTrajectory;
278 % generate the sensor data
279 data = obj.generate(traj.acc, traj.gyr, traj.ori);
282 % ====================================================================== %
283 %> @brief returns the parameters of the JUMP sensor.
285 %> getParams returns the parameters of the JUMP sensor. The parameters
286 %> are used to generate the erroneous sensor data.
290 %> @retval params (struct) - the parameters of the JUMP sensor
291 %> - params.mpu.acc (struct) - the parameters of the MPU9250 accelerometer
292 %> - params.mpu.gyr (struct) - the parameters of the MPU9250 gyroscope
293 %> - params.mpu.mag (struct) - the parameters of the MPU9250 magnetometer
294 %> - params.hig.acc (struct) - the parameters of the ADXL375 accelerometer
295 % ====================================================================== %
296 function params = getParams(obj)
298 params.mpu.acc = obj.mpu.Accelerometer;
299 params.mpu.gyr = obj.mpu.Gyroscope;
300 params.mpu.mag = obj.mpu.Magnetometer;
301 params.hig.acc = obj.hig.Accelerometer;
306 methods (Access = private)
308 % ====================================================================== %
309 %> @brief createMpu creates the MPU9250 sensor.
311 %> createMpu creates the MPU9250 sensor with the settings specified in
312 %> the properties of the JUMP sensor model.
318 %> @note in here the actual sensor is modeled with noise and bias. See the
319 %> actual code for the parameters and explanations.
320 % ====================================================================== %
321 function createMpu(obj)
323 obj.mpu = imuSensor('accel-gyro-mag', ... % sensor type
324 "ReferenceFrame", "NED", ... % reference frame for JUMP sensors
325 "SampleRate", obj.sampleRate, ... % sample rate
326 "Temperature", 25, ... % temperature (tunable)
327 "MagneticField", [21.8719 1.2697, 42.8197] ... % magnetic field at 46.8N, 8.23E, 500m (GPS) (NED, according to Magbnetic Field Estimated Values of NOAA)
330 % accelerometer parameters
331 obj.mpu.Accelerometer.MeasurementRange = obj.accRange * 9.81; % m/s^2
332 obj.mpu.Accelerometer.Resolution = obj.accRange / 32768; % (m/s^2) / LSB
333 obj.mpu.Accelerometer.ConstantBias = [obj.rant(0.06), obj.rant(0.06), obj.rant(0.08)] * 9.81; % (m/s^2) (datasheet: 60,60,80 mg)
334 % sensitivity is modeled into the misalignment matrix
335 obj.mpu.Accelerometer.AxesMisalignment = obj.getMisalignmentMatrix(2,2,2,3); % (percent cross-axis sensitivity, percent overall sensitivity error)
336 obj.mpu.Accelerometer.NoiseDensity = [obj.rant(0.0025,0.0035,2),obj.rant(0.0025,0.0035,2),obj.rant(0.0055,0.0065,2)]; % (m/s^2) / sqrt(Hz) (datasheet: 300 µg/rtHz, value from allan variance experiments)
337 obj.mpu.Accelerometer.BiasInstability = 6e-3; % (m/s^2) (missing in datasheet, value from allan variance experiments)
338 obj.mpu.Accelerometer.RandomWalk = 3.9e-5; % (m/s^2) * sqrt(Hz) (missing in datasheet, value from allan variance experiments)
339 obj.mpu.Accelerometer.TemperatureBias = [obj.rant(0.0015), obj.rant(0.0015), obj.rant(0.0015)] * 9.81; % (m/s^2) / °C (datasheet: 1.5 mg/°C)
340 obj.mpu.Accelerometer.TemperatureScaleFactor = abs(obj.rant(0.026)); % (%/°C) (datasheet: 0.026 %/°C)
341 obj.mpu.Accelerometer.NoiseType = "single-sided";
343 % gyroscope parameters
344 obj.mpu.Gyroscope.MeasurementRange = obj.gyroRange * pi/180; % rad/s
345 obj.mpu.Gyroscope.Resolution = (obj.gyroRange * pi/180) / 32768; % (rad/s) / LSB
346 obj.mpu.Gyroscope.ConstantBias = [obj.rant(5), obj.rant(5), obj.rant(5)] * pi/180; % (rad/s) (datasheet: +-5 dps)
347 % sensitivity is modeled into the misalignment matrix
348 obj.mpu.Gyroscope.AxesMisalignment = obj.getMisalignmentMatrix(2,2,2,3); % (percent cross-axis sensitivity)
349 obj.mpu.Gyroscope.NoiseDensity = [obj.rant(4.7e-3,6e-3,2),obj.rant(4.7e-3,6e-3,2),obj.rant(4.7e-3,6e-3,2)] * pi/180; % (rad/s) / sqrt(Hz) (datasheet: 10 mdps/rtHz, value from allan variance experiments)
350 obj.mpu.Gyroscope.BiasInstability = 8.3e-3 * pi/180; % (rad/s) (missing in datasheet, value from allan variance experiments)
351 obj.mpu.Gyroscope.RandomWalk = 155.9e-6 * pi/180; % (rad/s) * sqrt(Hz) (missing in datasheet, value from allan variance experiments)
352 obj.mpu.Gyroscope.TemperatureBias = [obj.rant(0.48), obj.rant(0.48), obj.rant(0.48)] * pi / 180; % (rad/s) / °C (datasheet: (60 °/s) / 125 °C)
353 obj.mpu.Gyroscope.TemperatureScaleFactor = abs(obj.rant(3)); % (%/°C) (datasheet: 3 %/°C)
354 obj.mpu.Gyroscope.AccelerationBias = 0.1 * pi/180; % (rad/s) / (m/s^2) (NOTE: missing in datasheet, this is an estimation from https:
355 obj.mpu.Gyroscope.NoiseType = "single-sided";
357 % magnetometer parameters
358 obj.mpu.Magnetometer.MeasurementRange = 4800; % µT
359 obj.mpu.Magnetometer.Resolution = 9600 / 2^14; % µT / LSB
360 obj.mpu.Magnetometer.ConstantBias = obj.mpu.Magnetometer.Resolution * randn() * 500; % µT (datasheet: +-500 LSB)
361 obj.mpu.Magnetometer.AxesMisalignment = diag([100,100,100]); % (percent cross-axis sensitivity) (NOTE: missing in datasheet)
362 obj.mpu.Magnetometer.NoiseDensity = obj.rant(0.65,0.75,2); % µT / sqrt(Hz) (missing in datasheet, value from allan variance experiments)
363 obj.mpu.Magnetometer.BiasInstability = 0.3673; % µT (missing in datasheet, value from allan variance experiments)
364 obj.mpu.Magnetometer.RandomWalk = 0.01216; % µT * sqrt(Hz) (missing in datasheet, value from allan variance experiments)
365 % NOTE: temperature bias and scale factor are missing in the datasheet
366 obj.mpu.Magnetometer.NoiseType = "single-sided";
369 % ====================================================================== %
370 %> @brief createHig creates the ADXL375 sensor.
372 %> createHig creates the ADXL375 sensor with the settings specified in
373 %> the properties of the JUMP sensor model.
379 %> @note in here the actual sensor is modeled with noise and bias. See the
380 %> actual code for the parameters and explanations.
381 % ====================================================================== %
382 function obj = createHig(obj)
384 obj.hig = imuSensor('accel-gyro', ... % sensor type (NOTE: there is no actual gyro, but the imuSensor class requires it)
385 "ReferenceFrame", "NED", ... % reference frame for JUMP sensors
386 "SampleRate", obj.sampleRate, ... % sample rate
387 "Temperature", 25 ... % temperature (tunable)
390 % accelerometer parameters
391 obj.hig.Accelerometer.MeasurementRange = 200 * 9.81; % m/s^2 (datasheet: min 180g, typ: 200g)
392 obj.hig.Accelerometer.Resolution = 0.049 * 9.81; % (m/s^2) / LSB (datasheet: 49 mg/LSB)
393 obj.hig.Accelerometer.ConstantBias = [obj.rant(-0.4,0.4,1),obj.rant(-0.4,0.4,1),obj.rant(-0.4,0.4,1)] * 9.81; % (m/s^2) (datasheet: +- 0.4g 1 sigma)
394 obj.hig.Accelerometer.AxesMisalignment = obj.getMisalignmentMatrix(2.5); % (percent cross-axis sensitivity) (datasheet: 2.5%)
395 obj.hig.Accelerometer.NoiseDensity = obj.rant(0.0264,0.0381,2); % (m/s^2) / sqrt(Hz) (datasheet: 5 mg/rtHz, value from allan variance experiments)
396 obj.hig.Accelerometer.BiasInstability = 0.0038; % m/s^2 (missing in datasheet, value from allan variance experiments)
397 obj.hig.Accelerometer.RandomWalk = 174.95e-6; % (m/s^2) * sqrt(Hz) (missing in datasheet, value from allan variance experiments)
398 obj.hig.Accelerometer.TemperatureBias = abs(obj.rant(-0.01,0.01,1)) * 9.81; % (m/s^2) / °C (datasheet: 10 mg/°C)
399 obj.hig.Accelerometer.TemperatureScaleFactor = abs(obj.rant(-0.02,0.02,1)); % (%/°C) (datasheet: 0.02 %/°C)
400 obj.hig.Accelerometer.NoiseType = "single-sided";
404 % ====================================================================== %
405 %> @brief getPressureFromAltitude returns the pressure from the altitude.
407 %> getPressureFromAltitude returns the pressure from the altitude. The
408 %> pressure is modeled with the barometric formula and the actual sensor
409 %> is modeled with noise and bias.
412 %> @param alt (nx1
double) - the altitude in m
413 %> @param tmp (nx1
double, optional) - the temperature in °C (default: 25°C)
414 %> @param prs_sealvl (1x1
double, optional) - the pressure at sea level in mbar (default: 1013.25 mbar)
416 %> @retval mbar (nx1
double) - the pressure in mbar
418 %> @note the experiment 240213_pressure_membrane suggested a mean raise
419 %> time of 0.5s. Since this is approx the free fall duration (=0.49 s),
420 %> we assume that the pressure sensor is able to measure the pressure with
421 %> almost no delay. Therefore, we do not need to model the delay of the
424 %> @note formula from: https:
425 % ====================================================================== %
426 function mbar = getPressureFromAltitude(obj, alt, tmp, prs_sealvl)
431 alt (:,1)
double {mustBeInRange(alt, 0, 2000)}
432 tmp (:,1) double {mustBeInRange(tmp, -20, 40)} = repmat(25, numel(alt),1); % temperature in °C
433 prs_sealvl (1,1) double = 1013.25; % pressure at sea level in mbar
436 % downsample the altitude to the sample rate of the pressure sensor
437 downsample_ratio = obj.sampleRate / obj.sampleRatePressure;
438 alt_ds = downsample(alt, downsample_ratio);
439 tmp = downsample(tmp, downsample_ratio);
441 % calculate the pressure from the altitude (source: https:
443 % Formula: P = P0 * exp(-g * M * (h - h0) / (R * T))
446 % P : pressure at altitude h in Pascal
447 % P0: pressure at sea level in Pascal
448 % T : temperature at h in Kelvin
449 % g : acceleration due to gravity in m/s^2
450 % M : molar mass of Earth's air in kg/mol
451 % R: universal gas constant in J/(mol*K)
453 P0 = prs_sealvl * 100; % convert to Pascal
454 T = tmp + 273.15; % convert to Kelvin
456 M = 0.0289644; % kg/mol
457 R = 8.31432; % J/(mol*K)
459 P = P0 * exp(-g * M .* h ./ (R * T));
460 mbar = P / 100; % convert to mbar
462 % the experiment 240213_pressure_membrane suggested a mean raise time of 0.5s.
463 % since this is approx the free fall duration (=0.49 s), we assume that the
464 % pressure sensor is able to measure the pressure with almost no delay.
465 % Therefore, we do not need to model the delay of the sensor.
467 % model the actual sensor
468 resolution = 0.027; % mbar / LSB
469 noiseDensity = 0.0109; % mbar / sqrt(Hz) (from allan variance experiments)
470 biasInstability = 0.013; % mbar (from allan variance experiments)
471 randomWalk = 0.003; % mbar * sqrt(Hz) (from allan variance experiments)
474 noise = obj.generateNoise(length(mbar), noiseDensity, randomWalk, biasInstability, obj.sampleRatePressure);
476 bias = obj.rant(-1.5,1.5); % mbar
477 mbar = mbar + bias + noise; % add noise and bias
478 mbar = mbar / resolution; % convert to LSB
479 mbar = round(mbar); % round to integer
480 mbar = mbar * resolution; % convert back to mbar
482 % upsample the pressure to the sample rate of the MPU (filling with the last value)
483 mbarOut = nan(length(alt),1);
484 mbarOut(1:downsample_ratio:end) = mbar;
486 % mbar = fillmissing(mbarOut, 'previous'); % the new
jumpReadData function handles this better, therefore not needed anymore.
489 % ====================================================================== %
490 %> @brief getSensorTemperature returns the sensor temperature.
492 %> getSensorTemperature returns the sensor temperature with the actual
493 %> sensor noise and bias.
496 %> @param tmp_true (nx1
double) - the true temperature in °C
498 %> @retval degC (nx1
double) - the sensor temperature in °C
499 % ====================================================================== %
500 function degC = getSensorTemperature(obj, tmp_true)
505 tmp_true (:,1)
double {mustBeInRange(tmp_true, -20, 40)} = repmat(25, numel(alt),1); % temperature in °C
508 % downsample the temperature to the sample rate of the pressure sensor
509 downsample_ratio = obj.sampleRate / obj.sampleRatePressure;
510 tmp_true_ds = downsample(tmp_true, downsample_ratio);
512 % model the actual sensor
513 resolution = 0.01; % °C / LSB
514 noiseDensity = 0.0013; % °C / sqrt(Hz) (from allan variance experiments)
515 biasInstability = 0.00158; % °C (from allan variance experiments)
516 randomWalk = 0.0005; % °C * sqrt(Hz) (from allan variance experiments)
519 noise = obj.generateNoise(length(tmp_true_ds), noiseDensity, randomWalk, biasInstability, obj.sampleRatePressure);
521 bias = obj.rant(-0.8,0.8); % °C
522 degC = tmp_true_ds + bias + noise; % add noise and bias
524 % quantize the temperature
525 degC = degC / resolution; % convert to LSB
526 degC = round(degC); % round to integer
527 degC = degC * resolution; % convert back to °C
529 % upsample the temperature to the sample rate of the MPU (filling with the last value)
530 degCOut = nan(length(tmp_true),1);
531 degCOut(1:downsample_ratio:end) = degC;
533 %degC = fillmissing(degCOut,
'previous'); % the
new jumpReadData function handles
this better, therefore not needed anymore.
540 methods (Access =
private, Static)
542 % ====================================================================== %
543 %> @brief getMisalignmentMatrix returns a misalignment matrix
for the
544 %> accelerometer or gyroscope.
546 %> getMisalignmentMatrix returns a misalignment matrix
for the
547 %> accelerometer or gyroscope. The misalignment matrix is a 3x3 matrix
548 %> which is used to rotate the sensor axes to the reference frame. The
549 %> misalignment matrix is generated randomly.
551 %> @param x (1x1
double) - the typical misalignment of the x-axis in percent
552 %> @param y (1x1 double) - the typical misalignment of the y-axis in percent
553 %> @param z (1x1 double) - the typical misalignment of the z-axis in percent
554 %> @param sensitivity (1x1 double) - the overall sensitivity error in percent
556 %> @retval M (3x3 double) - the misalignment matrix in percent
558 %> @note the misalignment matrix is generated with the following steps:
559 %> 1. generate the three axis in perfect alignment, but with sensitivity errors
560 %> 2. generate the misalignment angles
561 %> 3. generate the misalignment rotation
562 %> 4. rotate the axes (attention, the rotx function works with degrees!)
563 %> 5. create the misalignment matrix
564 % ====================================================================== %
565 function M = getMisalignmentMatrix(x, y, z, sensitivity)
567 % check input arguments
578 sensitivity = sensitivity / 100;
580 % define the three axis in perfect alignment, but with sensitivity errors
585 % generate the misalignment angles
590 % generate the misalignment rotation
591 rot_x = rand() * 360;
592 rot_y = rand() * 360;
593 rot_z = rand() * 360;
595 % rotate the axes (attention, the rotx function works with degrees!)
596 ax_x = rotx(rot_x) * ( roty(theta_x) * ax_x );
597 ax_y = roty(rot_y) * ( rotz(theta_y) * ax_y );
598 ax_z = rotz(rot_z) * ( rotx(theta_z) * ax_z );
600 % create the misalignment matrix
601 M = [ax_x'; ax_y'; ax_z'] * 100;
605 % ====================================================================== %
606 %> @brief rant returns a typical value in the sense of datasheet typical
609 %> rant(lower) returns a typical random number with a symmetric
610 %> distribution around zero, where the range is defined by the lower
611 %> bound. The random number is gaussian distributed with lower been the
612 %> two standard deviation range.
614 %> rant(lower, upper) returns a typical random number with a gaussian
615 %> distribution with the given range.
617 %> rant(lower, upper, nstd) returns a typical random number with a
618 %> gaussian distribution with nstd standard deviations.
620 %> @param lower (1x1
double) - the lower bound of the distribution
621 %> @param upper (1x1
double) - the upper bound of the distribution
622 %> @param nstd (1x1
double) - the number of standard deviations
624 %> @retval number (1x1
double) - the random number
625 function number = rant(lower, upper, nstd)
627 % check input arguments
630 upper (1,1)
double = -lower;
631 nstd (1,1)
double = 2;
635 [lower, upper] = deal(upper, lower);
638 % gaussian distribution
639 mu = (lower + upper) / 2;
640 sigma = (upper - lower) / (2 * nstd);
641 number = normrnd(mu, sigma);
645 % ====================================================================== %
646 %> @brief generateNoise generates noise with the given parameters.
648 %> generateNoise generates noise with the given parameters. The noise is
649 %> generated with the following model:
650 %> - white noise with the noise density N
651 %> - random walk with the coefficient K
652 %> - bias instability with the coefficient B
653 %> The noise is generated with the sample rate fs.
655 %> @param n (1x1
double) - the number of samples
656 %> @param N (1x1
double) - the noise density
657 %> @param K (1x1
double) - the random walk coefficient
658 %> @param B (1x1
double) - the bias instability coefficient
659 %> @param fs (1x1
double) - the sample rate
661 %> @retval noise (nx1
double) - the generated noise
662 % ====================================================================== %
663 function noise = generateNoise(n,N,K,B,fs)
665 % check input arguments
667 n (1,1)
double {mustBeInteger, mustBePositive}; % number of samples
668 N (1,1) double {mustBePositive}; % noise density
669 K (1,1) double {mustBePositive}; % random walk
670 B (1,1) double {mustBePositive}; % bias instability
671 fs (1,1) double {mustBeInteger, mustBePositive}; % sample rate
674 % scale the coefficients
675 N = N * sqrt(2); % single-sided noise
676 K = K * sqrt(2); % single-sided noise
677 B = B * sqrt(2); % single-sided noise
679 % define the PSD of the Noise
680 epsilon = 1e-6; % to avoid division by zero
681 PSD = @(f) N^2 + B^2 ./ (2*pi*f + epsilon) + K^2 ./ (2*pi*f + epsilon).^2;
684 f = (0:(n-1)/2) * fs / n;
687 white_noise = randn(n,1);
689 % one-sided fft of the white noise
690 Y = fft(white_noise);
692 % set the DC component to zero
695 % adjust the amplitude according to the PSD
696 Y(1:length(f)) = Y(1:length(f)) .* sqrt(PSD(f))
';
698 % if the signal length is even, the Nyquist frequency component is real
700 Y (n/2+1) = real(Y(n/2+1));
704 noise = ifft(Y, 'symmetric
');