Van's Air Force

The definitive Van's Aircraft support community! Buying, building or flying an RV? Join our exclusive family of mentors and enthusiasts!

DIY dynamic prop balancer?

Here's some math explaining the "Influence Coefficient" mentioned in a previous post, which I haven't verified:
How does a prop balancer calculate an "influence coefficient" using an engine run with no weight and an engine run with a trial weight
How a Prop Balancer Calculates an Influence Coefficient

This is a classic single-plane balancing problem. The influence coefficient (also called the balancing sensitivity) answers: "How much does the vibration vector change per unit of trial weight, at a given angular position?" It's measured empirically because you can't easily model the system analytically (flexible mounts, complex geometry, etc.).

The Two Runs​


Run 1 — Baseline (no weight)
Measure the vibration at the target RPM. The result is a vector with both magnitude and phase:
  • A₀ = vibration amplitude (e.g., in/s or mm/s)
  • φ₀ = phase angle relative to the tachometer once-per-rev trigger pulse

Run 2 — Trial weight run
Attach a known trial weight W at a known angular position θ_W on the prop, then run again at the same RPM to get a new vibration amplitude and phase.

Calculating the Influence Coefficient​


The influence coefficient IC is the change in vibration caused by the trial weight, divided by the trial weight vector:

Code:
IC = (V1 - V0) / (W at angle θ_W)

This is complex-number division where:
  • Numerator: the change in vibration caused by the trial weight (ΔV = V1 − V0)
  • Denominator: the trial weight expressed as a vector (magnitude × angular position)

The result IC is a complex number with units of (vibration)/(gram), e.g., (in/s)/gram, and encodes:
  • How sensitive the system is to added mass
  • The phase lag between where you put weight and where the vibration effect appears

Using IC to Find the Correction Weight​


Code:
W_correction = -(V0 / IC)

The magnitude tells you how many grams to add. The angle tells you where on the prop to place it, in the same angular reference frame as your tachometer trigger.

Worked Example​


QuantityValue
Baseline vibration V00.15 in/s @ 45°
Trial weight W10g @ 0°
Post-trial vibration V10.22 in/s @ 112°

Step 1 — Convert to rectangular (complex) form:
Code:
V0 = 0.15·cos(45°) + j·0.15·sin(45°) = 0.106 + 0.106j
V1 = 0.22·cos(112°) + j·0.22·sin(112°) = -0.082 + 0.204j
W  = 10·cos(0°) + j·10·sin(0°) = 10 + 0j

Step 2 — Compute ΔV:
Code:
ΔV = (-0.082 + 0.204j) - (0.106 + 0.106j) = -0.188 + 0.098j

Step 3 — Compute IC:
Code:
IC = (-0.188 + 0.098j) / (10 + 0j) = -0.0188 + 0.0098j
   = 0.0212 (in/s)/g @ 152°

Step 4 — Compute correction weight:
Code:
W_corr = -(0.106 + 0.106j) / (-0.0188 + 0.0098j)
Performing the complex division gives you a magnitude (grams) and angle (clock position on the prop).

Why Phase Matters So Much​


Phase measurement requires a reflective tape + optical pickup (or mag sensor) giving a once-per-rev pulse. The balancer measures the time delay between this trigger and the vibration peak, converting it to degrees. Without phase you only know how bad the imbalance is, not where to correct it.

Practical Notes​

  • The trial weight should cause a noticeable change — typically 20–50% change in amplitude or a clear phase shift. Too small and measurement noise dominates.
  • RPM must be identical between runs — even small differences change vibration amplitude independently of balance.
  • The IC is only valid at the RPM where it was measured, since structural dynamics are speed-dependent.
  • Commercial balancers (Dynavibe, etc.) do all this math internally and just tell you grams and clock position.

Complex Number Division

To divide two complex numbers, you can't just divide real and imaginary parts separately (unlike addition). Instead you use a trick: multiply top and bottom by the complex conjugate of the denominator, which eliminates the imaginary part from the denominator and gives you a plain real number to divide by.

The Complex Conjugate​


The conjugate of a complex number simply flips the sign of the imaginary part:

Code:
If z = a + bj
Then z* = a - bj

The useful property is that a number multiplied by its conjugate is always a real number:

Code:
(a + bj)(a - bj) = a² + b²

(The cross terms cancel, and j² = -1 turns -b²j² into +b².)

The Division Procedure​


To compute (a + bj) / (c + dj):

Code:
Step 1: Multiply numerator and denominator by the conjugate of the denominator (c - dj)

        (a + bj)   (a + bj)(c - dj)
        -------- = ----------------
        (c + dj)   (c + dj)(c - dj)

Step 2: Expand the numerator using FOIL

        (a + bj)(c - dj) = ac - adj + bcj - bdj²
                         = ac - adj + bcj + bd      (since j² = -1)
                         = (ac + bd) + (bc - ad)j

Step 3: Expand the denominator (always gives a real number)

        (c + dj)(c - dj) = c² + d²

Step 4: Separate into real and imaginary parts

        Result = (ac + bd)    +    (bc - ad) j
                 ---------         ---------
                  c² + d²           c² + d²

Worked Example — From the Prop Balancer Calculation​


We need to divide V0 by IC to find the correction weight:

Code:
V0 =  0.106 + 0.106j
IC = -0.0188 + 0.0098j

Identify a, b, c, d:

Code:
a =  0.106   b =  0.106
c = -0.0188  d =  0.0098

Step 1 — Compute the numerator terms:

Code:
ac + bd = (0.106)(-0.0188) + (0.106)(0.0098)
        = -0.001993 + 0.001039
        = -0.000954

bc - ad = (0.106)(-0.0188) - (0.106)(0.0098)
        = -0.001993 - 0.001039
        = -0.003032

Step 2 — Compute the denominator:

Code:
c² + d² = (-0.0188)² + (0.0098)²
        = 0.000354 + 0.000096
        = 0.000450

Step 3 — Divide:

Code:
V0 / IC = (-0.000954 / 0.000450) + (-0.003032 / 0.000450)j
        = -2.12 - 6.74j

Step 4 — The correction weight is the negative of this:

Code:
W_corr = -(-2.12 - 6.74j) = 2.12 + 6.74j

Converting the Result to Polar Form (Magnitude and Angle)​


The balancer displays the result as grams at a clock position, which is polar form:

Code:
Magnitude = √(real² + imag²)
          = √(2.12² + 6.74²)
          = √(4.49 + 45.43)
          = √49.92
          = 7.07 grams

Angle     = atan2(imag, real)
          = atan2(6.74, 2.12)
          = 72.6°

So the balancer would call for 7.07 grams at 72.6° on the prop.

Doing it in Polar Form Directly​


If both numbers are already in polar form (magnitude @ angle), division is even simpler — divide the magnitudes and subtract the angles:

Code:
If P = M_p @ φ_p   and   Q = M_q @ φ_q

Then P / Q = (M_p / M_q) @ (φ_p - φ_q)

This is why engineers often stay in polar form for division and multiplication, and switch to rectangular form only for addition and subtraction.
 
Well - I have a solution. Not sure if it works - that will require another flight, but here is what I have.

Yesterday I got baseline data with no weights installed, today I added 23.4g at hole 4 and flew again. Theses are the resultant solutions:


View attachment 117280

View attachment 117282

View attachment 117281

I'll add these weights and see what I get. The ACES solution 2 years ago was 19.7 g at hole 6@2270 rpm. So I'm in the right universe.

I let Gemini do the math here - and cross-checked it with Claude -they argued a bit, but they finally agreed. This is a low speed- cruise balance. Later, I'll try one at my high speed 2270 rpm cruise as well.
Offhand that doesn't make sense. Would have thought you'd have to add a weight opposite the initial imbalance: 206.5 degrees. If this because of the 90 degree shift mentioned earlier?
 
From a conversation with Claude asking why placing an 126 inch-gram test weight did not significantly alter ips and only altered angle (still not satisfied with the answer -- I think it's due to incorrect measurement results) it appears that needed compensation weight is largely dependent on system weight, that is: weight of prop and spinner. Not sure that makes sense. Intuitively we should be able to exclude the weight of the balanced system and only need to consider the imbalance mass.

This makes me wonder how the tire balancers work you see at auto tire places.
They are typically able to place correct weights at the correct angles on first try.
Do they have a built-in scale that measures the weight of rim and tire?
Or is the total system weight actually irrelevant?
Is it possible that someone is trying to make balancing more complex by trying to add irrelevant factors?

Edit: By irrelevant I mean factors that zero out. Yes, we could include the Earth's rotation on its axis, rotation around the Sun, the Sun's movement relative to the galaxy, rotation of galaxy ...
 
Last edited:
Offhand that doesn't make sense. Would have thought you'd have to add a weight opposite the initial imbalance: 206.5 degrees. If this because of the 90 degree shift mentioned earlier?
I agree - i actually did some more data crunching last night and removed some data points with really low ips that were questionable. The new math shows weight at hole 10. I'll try them both - but hole 10 seems more logical. Really gusty here today, so probably wont fly. I will report back.
 
Offhand that doesn't make sense. Would have thought you'd have to add a weight opposite the initial imbalance: 206.5 degrees. If this because of the 90 degree shift mentioned earlier?
Go back and read the last two spoilers in Post #197.
 
Well - no joy on the solution, but, I think I know why. i pulled a raw data log off the adxl355, and it is hitting its 8g limit on many cycles. 1) I'll try to vibration isolate the mount and 2) the ADXL firmware runs at 4kHz ODR (1kHz bandwidth). Dropping to 1kHz ODR (500Hz bandwidth) should cut broadband high-frequency engine noise while still resolving the imbalance signal with margin. This means 1000 samples/sec instead of 4000, but still ~29 samples per revolution.

My balancing algorithm is working correctly and the Pmag timing after filtering is really solid. The sensor is the problem - I Isolated the sensor this morning and on a ground run, the clipping is gone, and the phase data looks better. I'll fly a baseline and trial weight flight tomorrow. I got blinded thinking the tach was the issue and it seems it was the accel..........

Stay tuned! or don't, I know this is pretty geeky.......
 
Well - no joy on the solution, but, I think I know why. i pulled a raw data log off the adxl355, and it is hitting its 8g limit on many cycles. 1) I'll try to vibration isolate the mount and 2) the ADXL firmware runs at 4kHz ODR (1kHz bandwidth). Dropping to 1kHz ODR (500Hz bandwidth) should cut broadband high-frequency engine noise while still resolving the imbalance signal with margin. This means 1000 samples/sec instead of 4000, but still ~29 samples per revolution.

My balancing algorithm is working correctly and the Pmag timing after filtering is really solid. The sensor is the problem - I Isolated the sensor this morning and on a ground run, the clipping is gone, and the phase data looks better. I'll fly a baseline and trial weight flight tomorrow. I got blinded thinking the tach was the issue and it seems it was the accel..........

Stay tuned! or don't, I know this is pretty geeky.......
Not sure if this will work in your algorithm but I'm excluding any sample exceeding 0.25Gs initially. But then I'm continuously averaging for minutes in each bin to average out noise (engine vibrations) and subtract DC levels.. At the moment I've cut it down to 36 10-degree bins but will probably change back to 72 5-degree bins or more and live with longer averaging time. But Cos(10) is 0.985 or 2.5% error over a 10-degree span, so not really important at this stage of the game. Also apply a per-slot sigma rejection using running variance as per suggestion and help from Claude. (I really need to learn about statistics.)

Did first flight in smooth air this morning and results converged on 0.58 ips! Time to pull the prop and do a new static balancing.

// ===========================================================================
// loop() (core 1, priority 2)
// Runs at up to 4000 Hz driven by ADXL355 DRDY interrupt flag.
// Reads accelerometer, assigns sample to angular slot, applies outlier
// rejection, accumulates into slot buffers.
// ===========================================================================
void loop() {
if (!bDataReady) return;
bDataReady = false;
ADXL355Data d;
accel.readData(d);
temp = d.temp_c; //get sensor temperature in celsius
// Atomically snapshot ISR-written timing variables
uint64_t local_period, local_last_tach, local_sample_time;
uint16_t local_rpm;
portENTER_CRITICAL(&mux);
local_period = period_us;
local_last_tach = last_tach_time_us;
local_sample_time = sample_time_us;
local_rpm = rpm;
portEXIT_CRITICAL(&mux);
// --- Gate checks --------------------------------------------------------
if (local_period == 0) return; // no tach yet
if (local_sample_time < local_last_tach) return; // sample before tach edge
//only accept samples that are within (narrow) RPM range because RPM is used to calculate ips
if (local_rpm < (TargetRPM - TargetRPMTolerance)) return;
if (local_rpm > (TargetRPM + TargetRPMTolerance)) return;
// --- Slot assignment ----------------------------------------------------
uint64_t delta = local_sample_time - local_last_tach;
if (delta >= local_period) return; // wrapped past one rev
int index = (int)((SAMPLE_SLOTS * delta) / local_period);
if (index < 0 || index >= SAMPLE_SLOTS) return; // safety clamp
// --- Outlier rejection --------------------------------------------------
uint32_t n = sample_buffer[index].NumberOfSamples;
bool accept = false;
if (n < MIN_BOOTSTRAP) {
// Bootstrap phase: circular hard threshold (avoids square-boundary bias)
int32_t sx = d.x, sy = d.y;
accept = ((sx * sx + sy * sy) < BOOTSTRAP_THRESH_SQ);
} else {
// Per-slot sigma rejection using running variance
// Variance = E[x²] - E[x]² (computed from integer accumulators)
float mx = (float)(sample_buffer[index].x) / n;
float my = (float)(sample_buffer[index].y) / n;
float vx = (float)(sample_buffer[index].x2) / n - (mx * mx);
float vy = (float)(sample_buffer[index].y2) / n - (my * my);
float dx = d.x - mx;
float dy = d.y - my;
// Accept if within N_SIGMA_SQ on both axes.
// If variance is near zero (slot fully converged) fall back to
// bootstrap threshold to avoid rejecting everything.
bool x_ok = (vx < 1.0f) ? ((dx * dx) < BOOTSTRAP_THRESH_SQ) :
((dx * dx) < N_SIGMA_SQ * vx);
bool y_ok = (vy < 1.0f) ? ((dy * dy) < BOOTSTRAP_THRESH_SQ) :
((dy * dy) < N_SIGMA_SQ * vy);
accept = x_ok && y_ok;
}
// --- Accumulate or count rejection -------------------------------------
if (accept) {
sample_buffer[index].NumberOfSamples += 1;
sample_buffer[index].x += d.x;
sample_buffer[index].y += d.y;
sample_buffer[index].x2 += (int64_t)d.x * d.x;
sample_buffer[index].y2 += (int64_t)d.y * d.y;
} else {
sample_buffer[index].rejected += 1;
}
}
1778790428481.png
Edit: Angle determination on my installation is probably made more difficult due to engine now stiffly mounted to air frame. Yeah, the Wankel rotary may be smooth but can't count on prop being, May reinstall the engine mount rubber insulators.
 
Last edited:
Not sure if this will work in your algorithm but I'm excluding any sample exceeding 0.25Gs initially. But then I'm continuously averaging for minutes in each bin to average out noise (engine vibrations) and subtract DC levels.. At the moment I've cut it down to 36 10-degree bins but will probably change back to 72 5-degree bins or more and live with longer averaging time. But Cos(10) is 0.985 or 2.5% error over a 10-degree span, so not really important at this stage of the game. Also apply a per-slot sigma rejection using running variance as per suggestion and help from Claude. (I really need to learn about statistics.)

Did first flight in smooth air this morning and results converged on 0.58 ips! Time to pull the prop and do a new static balancing.

// ===========================================================================
// loop() (core 1, priority 2)
// Runs at up to 4000 Hz driven by ADXL355 DRDY interrupt flag.
// Reads accelerometer, assigns sample to angular slot, applies outlier
// rejection, accumulates into slot buffers.
// ===========================================================================
void loop() {
if (!bDataReady) return;
bDataReady = false;
ADXL355Data d;
accel.readData(d);
temp = d.temp_c; //get sensor temperature in celsius
// Atomically snapshot ISR-written timing variables
uint64_t local_period, local_last_tach, local_sample_time;
uint16_t local_rpm;
portENTER_CRITICAL(&mux);
local_period = period_us;
local_last_tach = last_tach_time_us;
local_sample_time = sample_time_us;
local_rpm = rpm;
portEXIT_CRITICAL(&mux);
// --- Gate checks --------------------------------------------------------
if (local_period == 0) return; // no tach yet
if (local_sample_time < local_last_tach) return; // sample before tach edge
//only accept samples that are within (narrow) RPM range because RPM is used to calculate ips
if (local_rpm < (TargetRPM - TargetRPMTolerance)) return;
if (local_rpm > (TargetRPM + TargetRPMTolerance)) return;
// --- Slot assignment ----------------------------------------------------
uint64_t delta = local_sample_time - local_last_tach;
if (delta >= local_period) return; // wrapped past one rev
int index = (int)((SAMPLE_SLOTS * delta) / local_period);
if (index < 0 || index >= SAMPLE_SLOTS) return; // safety clamp
// --- Outlier rejection --------------------------------------------------
uint32_t n = sample_buffer[index].NumberOfSamples;
bool accept = false;
if (n < MIN_BOOTSTRAP) {
// Bootstrap phase: circular hard threshold (avoids square-boundary bias)
int32_t sx = d.x, sy = d.y;
accept = ((sx * sx + sy * sy) < BOOTSTRAP_THRESH_SQ);
} else {
// Per-slot sigma rejection using running variance
// Variance = E[x²] - E[x]² (computed from integer accumulators)
float mx = (float)(sample_buffer[index].x) / n;
float my = (float)(sample_buffer[index].y) / n;
float vx = (float)(sample_buffer[index].x2) / n - (mx * mx);
float vy = (float)(sample_buffer[index].y2) / n - (my * my);
float dx = d.x - mx;
float dy = d.y - my;
// Accept if within N_SIGMA_SQ on both axes.
// If variance is near zero (slot fully converged) fall back to
// bootstrap threshold to avoid rejecting everything.
bool x_ok = (vx < 1.0f) ? ((dx * dx) < BOOTSTRAP_THRESH_SQ) :
((dx * dx) < N_SIGMA_SQ * vx);
bool y_ok = (vy < 1.0f) ? ((dy * dy) < BOOTSTRAP_THRESH_SQ) :
((dy * dy) < N_SIGMA_SQ * vy);
accept = x_ok && y_ok;
}
// --- Accumulate or count rejection -------------------------------------
if (accept) {
sample_buffer[index].NumberOfSamples += 1;
sample_buffer[index].x += d.x;
sample_buffer[index].y += d.y;
sample_buffer[index].x2 += (int64_t)d.x * d.x;
sample_buffer[index].y2 += (int64_t)d.y * d.y;
} else {
sample_buffer[index].rejected += 1;
}
}
View attachment 117605
Edit: Angle determination on my installation is probably made more difficult due to engine now stiffly mounted to air frame. Yeah, the Wankel rotary may be smooth but can't count on prop being, May reinstall the engine mount rubber insulators.
Thanks Finn! I am using a diff method to find a solution - you are using a Time Domain Averaging (TDA) approach (binning), while I am using a Frequency Domain DFT (Single-Bin DFT) approach. That sounds like I might know what I am talking about - I know a little.... very little.

Both methods aim for the same goal—extracting the 1× prop signal from the noise. Now that I might have the clipping problem fixed - I'll test my method again. That said you are getting answers! Congrats! I'll report back after I fly tomorrow.
 
Well - no joy on the solution, but, I think I know why. i pulled a raw data log off the adxl355, and it is hitting its 8g limit on many cycles. 1) I'll try to vibration isolate the mount and 2) the ADXL firmware runs at 4kHz ODR (1kHz bandwidth). Dropping to 1kHz ODR (500Hz bandwidth) should cut broadband high-frequency engine noise while still resolving the imbalance signal with margin. This means 1000 samples/sec instead of 4000, but still ~29 samples per revolution.

My balancing algorithm is working correctly and the Pmag timing after filtering is really solid. The sensor is the problem - I Isolated the sensor this morning and on a ground run, the clipping is gone, and the phase data looks better. I'll fly a baseline and trial weight flight tomorrow. I got blinded thinking the tach was the issue and it seems it was the accel..........

Stay tuned! or don't, I know this is pretty geeky.......
I had ran into clipping problems as well. (https://vansairforce.net/threads/diy-dynamic-prop-balancer.241927/post-1926382, https://vansairforce.net/threads/diy-dynamic-prop-balancer.241927/post-1930529). Back then I was still using the ADXL345 which goes up to +/- 16 g. In my case I saw instances of being +16g on one sample and -16g on the next. I suspect that wires (or PCBs) inside my enclosure were vibrating. With how I had everything packed inside the enclosure, some of these wires came into direct contact with the accelerometer. If they were vibrating and slapping up against the accelerometer, that would definitely explain the high acceleration, high frequency readings I was getting. When I switched to the ADXL355, I redesigned my mount and put effort into ensuring the wiring was isolated from the accelerometer.

With the the ADXL355 on my 912iS, I'm still get occasional clipping on my X-axis (inline with cylinder movement). Looking at my latest data, over the span of a 73 second full throttle ground test I got 28 samples that may have clipped. I'm sampling at 2000 samples per second, so that's 28 samples out of 148442 (<0.02%).

If you now have raw data log files (and wouldn't mind sharing), I would like to run them through some of the analysis scripts I've been building. The most interesting data sets would be 10-60 seconds at a steady cruise RPM, and a slow sweep from idle to full throttle. I've got some of my log data here if anyone wants to take a look.
 
I had ran into clipping problems as well. (https://vansairforce.net/threads/diy-dynamic-prop-balancer.241927/post-1926382, https://vansairforce.net/threads/diy-dynamic-prop-balancer.241927/post-1930529). Back then I was still using the ADXL345 which goes up to +/- 16 g. In my case I saw instances of being +16g on one sample and -16g on the next. I suspect that wires (or PCBs) inside my enclosure were vibrating. With how I had everything packed inside the enclosure, some of these wires came into direct contact with the accelerometer. If they were vibrating and slapping up against the accelerometer, that would definitely explain the high acceleration, high frequency readings I was getting. When I switched to the ADXL355, I redesigned my mount and put effort into ensuring the wiring was isolated from the accelerometer.

With the the ADXL355 on my 912iS, I'm still get occasional clipping on my X-axis (inline with cylinder movement). Looking at my latest data, over the span of a 73 second full throttle ground test I got 28 samples that may have clipped. I'm sampling at 2000 samples per second, so that's 28 samples out of 148442 (<0.02%).

If you now have raw data log files (and wouldn't mind sharing), I would like to run them through some of the analysis scripts I've been building. The most interesting data sets would be 10-60 seconds at a steady cruise RPM, and a slow sweep from idle to full throttle. I've got some of my log data here if anyone wants to take a look.
I was not able to fly this morning, but will tomorrow. Here are 3 raw files of a ground run this morning - tell me if this works or what else you might need.

The isolation I used is 2 thin neoprene washers from the HW store. I had used 1/8" baffle material, but it excited the 2x band by 23x! Clearly too floppy...... the compressed neoprene looks better on ground runs.

I appreciate all the collaboration here! Below is the polar plot with 25g on hole 10 (300deg) - looks good - but it is at high idle. Will get real data tomorrow.

1778870453882.png
 

Attachments

I was not able to fly this morning, but will tomorrow. Here are 3 raw files of a ground run this morning - tell me if this works or what else you might need.
View attachment 117727
Looking at your log files, it looks like there's three files all with a duration of around two seconds. I'll refer to these file by their timestamps, 1425, 1427, and 1428.

One difference to be aware of between our log files is: you provide degrees of rotation, while I just provide time of the last tach signal. It wasn't a problem to convert between the two, but there is a chance errors were made in the conversion process. Although it's much more likely that my largely untested analysis scripts would be the source of errors.

1425
Quality 1425.png
1427
Quality 1427.png
1428
Quality 1428.png

The first graph in these is the sample distribution. There are a few problems here although not all of them show on the graph. I don't know what your trigger is for recording a sample, but there are several instances of taking an extra sample. Here is one such location.

Screenshot 2026-05-15 at 6.25.17 PM.png
For the most part samples are consistent at 992 - 993 us. At this location there's a repeated sample that occurs 334 us after the previous sample and 659 us before the next. There are several examples of these in each log file. To make them easier to spot I've applied conditional formatting to the spreadsheet. Red is <900us, Green is >1100us. You can find these spreadsheets in the RV9 subfolder of my previously shared log files. The .CSV files are your provided files, .reformatted are the files I've modified.

1428 also has a different problem. Here you have samples that appear to jump back in time. You also have two different samples that are only a microsecond apart.

Screenshot 2026-05-15 at 6.05.51 PM.png

Your RPMs look good. I'm not detecting any clipping.
 
I was not able to fly this morning, but will tomorrow. Here are 3 raw files of a ground run this morning - tell me if this works or what else you might need.
Here's the graphs created by my fft_analysis script. 1425 (1148 RPMs) has definite peaks at 2x and 4x. 1427 and 1428 (1376, 1387 RPMs) both have a good peak at 1x. But for some reason 1427 also has a peak at 2x, that 1428 doesn't have.

1425
FFT 1425.png

1427
FFT 1427.png

1428
FFT 1428.png
 
I was not able to fly this morning, but will tomorrow. Here are 3 raw files of a ground run this morning - tell me if this works or what else you might need.
Then there's sync_dft.py. This one is the least tested, and doesn't produce any fun graphs.

Since the provided log files are only two seconds, there may be as few as 38 revolutions recorded. Because of this I had to keep my window size small and wasn't able to monitor how consistent results were over time.

The text results for all three are below. Since you're only logging Y data, Ignore any parts referring to X axis or vector.
Code:
user@Admins-MacBook-Air PropBalancer % python3 sync_dft.py 1425.csv --window 32 --per-axis
Loading 1425.csv...
  2047 samples
  Sample period: 993.0 µs  (ODR ≈ 1007.0 Hz)
  Tach edges:    39
  Mean RPM:      1147.2  (min 1140.0, max 1153.0, std 0.27%)

  Detrend: per-revolution mean subtraction enabled (removes gravity bleed and sub-1× drift).

  Angle mode: firmware-bit-exact (uses previous-rev period and integer sample indexing).

=== PRIMARY DFT (window = 32 revs) ===
  Revs averaged   : 32  (1699 samples)
  RPM             : 1147.92  (std 0.240%)
  X amplitude     :    0.00 mg  →  0.0000 IPS
  Y amplitude     :   59.35 mg  →  0.1906 IPS
  X phase         :    0.0°
  Y phase         :   34.2°
  X−Y phase offset:  -34.2°  (90° = pure rotating imbalance)
  Vector amp      : 0.1348 IPS  @  0.0°
  SNR             : X  -inf dB   Y -28.4 dB

=== PER-AXIS ANALYSIS (window = 32 revs, RPM = 1147.9) ===

  METRIC                               X axis         Y axis
  ---------------------------- -------------- --------------
  1x DFT amplitude (mg)                  0.00          59.35
  1x DFT amplitude (IPS)               0.0000         0.1906
  1x phase (deg)                          0.0           34.2
  Total AC RMS (mg)                      0.00        1110.71
  Coherent power (mg^2)                  0.00        1761.37
  Total power (mg^2)                     0.00     1233684.03
  Coherence ratio (1x/total)           0.000%         0.143%
  Coherent SNR (dB)                      -inf          -28.4

  Interpretation:
    Coherence ratio = fraction of axis energy that's at 1x prop.
    Compare ratios between axes — the absolute value matters less
    than the X:Y ratio when broadband engine noise is high.

  → Y has signal; X is effectively zero. Check axis remap or single-axis (Y-only) reporting.
user@Admins-MacBook-Air PropBalancer % python3 sync_dft.py 1427.csv --window 32 --per-axis
Loading 1427.csv...
  2047 samples
  Sample period: 992.0 µs  (ODR ≈ 1008.1 Hz)
  Tach edges:    47
  Mean RPM:      1376.5  (min 1372.4, max 1384.7, std 0.18%)

  Detrend: per-revolution mean subtraction enabled (removes gravity bleed and sub-1× drift).

  Angle mode: firmware-bit-exact (uses previous-rev period and integer sample indexing).

=== PRIMARY DFT (window = 32 revs) ===
  Revs averaged   : 32  (1414 samples)
  RPM             : 1375.62  (std 0.148%)
  X amplitude     :    0.00 mg  →  0.0000 IPS
  Y amplitude     :  190.77 mg  →  0.5113 IPS
  X phase         :    0.0°
  Y phase         :  143.2°
  X−Y phase offset: -143.2°  (90° = pure rotating imbalance)
  Vector amp      : 0.3615 IPS  @  0.0°
  SNR             : X  -inf dB   Y -18.5 dB

=== PER-AXIS ANALYSIS (window = 32 revs, RPM = 1375.6) ===

  METRIC                               X axis         Y axis
  ---------------------------- -------------- --------------
  1x DFT amplitude (mg)                  0.00         190.77
  1x DFT amplitude (IPS)               0.0000         0.5113
  1x phase (deg)                          0.0          143.2
  Total AC RMS (mg)                      0.00        1141.75
  Coherent power (mg^2)                  0.00       18196.24
  Total power (mg^2)                     0.00     1303585.44
  Coherence ratio (1x/total)           0.000%         1.396%
  Coherent SNR (dB)                      -inf          -18.5

  Interpretation:
    Coherence ratio = fraction of axis energy that's at 1x prop.
    Compare ratios between axes — the absolute value matters less
    than the X:Y ratio when broadband engine noise is high.

  → Y has signal; X is effectively zero. Check axis remap or single-axis (Y-only) reporting.
user@Admins-MacBook-Air PropBalancer % python3 sync_dft.py 1428.csv --window 32 --per-axis
Loading 1428.csv...
  2044 samples
  Sample period: 992.0 µs  (ODR ≈ 1008.1 Hz)
  Tach edges:    47
  Mean RPM:      1388.4  (min 1381.9, max 1411.3, std 0.40%)

  Detrend: per-revolution mean subtraction enabled (removes gravity bleed and sub-1× drift).

  Angle mode: firmware-bit-exact (uses previous-rev period and integer sample indexing).

=== PRIMARY DFT (window = 32 revs) ===
  Revs averaged   : 32  (1405 samples)
  RPM             : 1388.49  (std 0.468%)
  X amplitude     :    0.00 mg  →  0.0000 IPS
  Y amplitude     :  243.59 mg  →  0.6468 IPS
  X phase         :    0.0°
  Y phase         :  150.9°
  X−Y phase offset: -150.9°  (90° = pure rotating imbalance)
  Vector amp      : 0.4574 IPS  @  0.0°
  SNR             : X  -inf dB   Y -16.1 dB

=== PER-AXIS ANALYSIS (window = 32 revs, RPM = 1388.5) ===

  METRIC                               X axis         Y axis
  ---------------------------- -------------- --------------
  1x DFT amplitude (mg)                  0.00         243.59
  1x DFT amplitude (IPS)               0.0000         0.6468
  1x phase (deg)                          0.0          150.9
  Total AC RMS (mg)                      0.00        1108.01
  Coherent power (mg^2)                  0.00       29667.07
  Total power (mg^2)                     0.00     1227678.66
  Coherence ratio (1x/total)           0.000%         2.417%
  Coherent SNR (dB)                      -inf          -16.1

  Interpretation:
    Coherence ratio = fraction of axis energy that's at 1x prop.
    Compare ratios between axes — the absolute value matters less
    than the X:Y ratio when broadband engine noise is high.

  → Y has signal; X is effectively zero. Check axis remap or single-axis (Y-only) reporting.
 
I was able to fly tonight and got my best data yet (I think).

1778903197281.png

Some analysis by Claude:(some of it goofy)

1x — the scatter cloud is clearly concentrated in the 240–300° sector with most points between hole 8 and hole 10. The spread is real but not random — you can see the cluster. The resultant at 258.2° (just past hole 8) is a solid representation of where the heavy spot actually is. The radial spread (amplitude varying 0.13–0.51 IPS) is the Dynafocal noise floor doing what it does.

2x — dramatically tighter clustering. Nearly every point falls between 24° and 114°, with the resultant sitting at 72.5° (between holes 3 and 4 in 2x frame). The 2x source is mechanically very stable — that's exactly what you'd expect from a fixed engine geometry effect like the crank counterweights or P-Mag firing pattern. Nothing concerning there.

The 90° relationship between the 1x heavy spot (258°) and the 2x phase (72.5° ÷ 2 = 36.3° in prop coordinates) is coincidental — they're independent phenomena with different physical sources.

I'll add 25g at hole 3 to get the IC trial and see what pops out of the math tomorrow.
 

Attachments

  • 1778903150223.png
    1778903150223.png
    51 KB · Views: 2
Here's the graphs created by my fft_analysis script. 1425 (1148 RPMs) has definite peaks at 2x and 4x. 1427 and 1428 (1376, 1387 RPMs) both have a good peak at 1x. But for some reason 1427 also has a peak at 2x, that 1428 doesn't have.

1425
View attachment 117752

1427
View attachment 117753

1428
View attachment 117754

Interesting plot. The y-axis says g rms- is that correct? The rms conversion is a running average that strips away the plus/minus nature of the raw data, the very thing you need for the FFT. It would be interesting to see the FFT of the amplitude g. The Welch window will smear out the spectral data, the other thing you need to get a clear picture of the harmonics. A Hann window is the general purpose standard.
 
Last edited:
All I can add is that using Claude to debug hardware problems will lead you to waste many hours of your life - it can be useful for software but that's it.
 
Interesting plot. The y-axis says g rms- is that correct? The rms conversion is a running average that strips away the plus/minus nature of the raw data, the very thing you need for the FFT. It would be interesting to see the FFT of the amplitude g. The Welch window will smear out the spectral data, the other thing you need to get a clear picture of the harmonics. A Hann window is the general purpose standard.
I'm relatively new to all of this. I don't completely understand what the difference is between a Welch and Hann window. But I am fascinated by the concept of analyzing signals in the frequency domain.

I'm familiar with RMS in standard electrical terms. Does it have a special meaning in regards to FFT? To perform the FFT I'm using SciPy.signal.welch() with window='hann' and scaling='spectrum' on the raw data. It's my understanding this returns the amplitude squared. So by taking the square root of the results we end up with g rms. To me, it seems like this would be what we would want.
 
Spent several days trying to statically balance the prop (hanging on fishing line through center hole).
Finally able to make a flight this morning.
Very, very disappointing. From 0.58 ips to 0.83 ips!
Also odd that flipped from 160 to 250 degrees for 13 seconds.
While hanging on the fishing line I did notice a tendency to tip to one side. Balanced tip vs tip but not around axis through tips..
But why would better balancing tip vs tip worsen the measured ips?
I did feel vibrations which I'd previously ignored or considered normal. So measured ips are probably not too wrong.

Also problems with phone connecting to and keeping connected to the ESP32.
(Pete, are you able to get reliable SPI transfer over 6' of wires at 5MHz?)

I guess try adding a test weight and new flight is next?

EDIT: Prop's off again. Found out I made the static balance worse. Should have handled prop's tendency to tilt about pivot point first (prop front facing up).
(I had removed the 25.4g @5" weight the builder had added to spinner back plate to handle that because I wanted a base line without weights.)

1779120765060.png
 

Attachments

Last edited:
I'm relatively new to all of this. I don't completely understand what the difference is between a Welch and Hann window. But I am fascinated by the concept of analyzing signals in the frequency domain.

I'm familiar with RMS in standard electrical terms. Does it have a special meaning in regards to FFT? To perform the FFT I'm using SciPy.signal.welch() with window='hann' and scaling='spectrum' on the raw data. It's my understanding this returns the amplitude squared. So by taking the square root of the results we end up with g rms. To me, it seems like this would be what we would want.

The standard FFT has the most precise amplitude and frequency information- it's the best representation of the data. Welch's method generates overlapping segments, windows those segments, performs the FFT on each window, squares the FFT magnitude, then calculates the mean (or median if you want) across all the windows. There's nothing inherently wrong with Welch's method, the RMS is a statistical tool used to clean up noisy data. I'm coming from the viewpoint that the engine vibration should be fairly repeatable over each revolution, unless it's starting to fly apart. If you're not seeing that repeatablity in the data, it's natural to ask what is the source of the noise? The peaks in your FFT are barely above the noise floor, even after apply Welch's method, is what got me thinking about the root cause. If you stick the vibration sensor to the hanger floor with beeswax, what does the FFT noise floor look like- that's where you use the RMS. It should be close to 0.75LSB. If the noise floor isn't low, then the polar plots (ips vs angle) will also have a wide variation. I'm trying to make the case that statistical tools should be used after the hardware is doing as expected- and that the noise floor is the most important spec of all.

Edit: The Rigol scope will calculate RMS- what is the ADXL345 pin Vsupply RMS noise (BW = sampling frequency- there are two ways to do this) on the hanger floor and on the airplane (engine running) using the probe ground spring?

Edit2: The sampled data beginning and ending values are seldom equal, which can cause artificial high frequency spurs in the FFT; the Hann window is like a "fade in" at the beginning of the sampled data, and a "fade-out" at the end of the data. The FFT deals with sine and cosine which describe circles in the complex plane- that circle needs to be continuous for the math to work at it's best.
 
Last edited:
The standard FFT has the most precise amplitude and frequency information- it's the best representation of the data. Welch's method generates overlapping segments, windows those segments, performs the FFT on each window, squares the FFT magnitude, then calculates the mean (or median if you want) across all the windows. There's nothing inherently wrong with Welch's method, the RMS is a statistical tool used to clean up noisy data. I'm coming from the viewpoint that the engine vibration should be fairly repeatable over each revolution, unless it's starting to fly apart. If you're not seeing that repeatablity in the data, it's natural to ask what is the source of the noise? The peaks in your FFT are barely above the noise floor, even after apply Welch's method, is what got me thinking about the root cause. If you stick the vibration sensor to the hanger floor with beeswax, what does the FFT noise floor look like- that's where you use the RMS. It should be close to 0.75LSB. If the noise floor isn't low, then the polar plots (ips vs angle) will also have a wide variation. I'm trying to make the case that statistical tools should be used after the hardware is doing as expected- and that the noise floor is the most important spec of all.
On my plots, g rms wasn't intended to represent any sort of statistical tool used to clean up the data. It was just the root mean square of the vibration amplitude coming out of the FFT. (For a perfect sine wave it would be 0.707 x peak).

I don't have any data collected with the accelerometer on the hangar floor, but I do have data collected with it mounted on the airplane inside the hangar on a calm day. The raw data is available here, in the spreadsheet named 'AirplaneOff 5/13'. Below is the FFT.
AirplaneOff.png
I don't know what's causing the ~80Hz spike on the X axis. I wasn't expecting to see that. Also be sure to the note the '1e-5' on the amplitude scale.

Edit: The Rigol scope will calculate RMS- what is the ADXL345 pin Vsupply RMS noise (BW = sampling frequency- there are two ways to do this) on the hanger floor and on the airplane (engine running) using the probe ground spring?
I'm no longer using the ADXL345, I have switched to the ADXL355. Coming into the eval board it's 8 mV. Going into the ADXL355 I'm measuring 2.6 mV.

Keep in mind that the data presented on this post is from my test setup. Other than the sensor, it's an entirely different setup from what the other's here are testing. This includes the plots you commented on just a few days ago. Those were from data provided by petehowell. So this data should be evaluated separately from that.
 
On my plots, g rms wasn't intended to represent any sort of statistical tool used to clean up the data. It was just the root mean square of the vibration amplitude coming out of the FFT. (For a perfect sine wave it would be 0.707 x peak).

I don't have any data collected with the accelerometer on the hangar floor, but I do have data collected with it mounted on the airplane inside the hangar on a calm day. The raw data is available here, in the spreadsheet named 'AirplaneOff 5/13'. Below is the FFT.
View attachment 118107
I don't know what's causing the ~80Hz spike on the X axis. I wasn't expecting to see that. Also be sure to the note the '1e-5' on the amplitude scale.


I'm no longer using the ADXL345, I have switched to the ADXL355. Coming into the eval board it's 8 mV. Going into the ADXL355 I'm measuring 2.6 mV.

Keep in mind that the data presented on this post is from my test setup. Other than the sensor, it's an entirely different setup from what the other's here are testing. This includes the plots you commented on just a few days ago. Those were from data provided by petehowell. So this data should be evaluated separately from that.

Your rms noise below looks very good compared to the adxl355 data sheet of 22.5 ug/rtHz
Your power supply noise of 2.6 mv: that's the pk to pk or rms? Either sounds good.
SciPy.signal.welch() does the statistical analysis under the covers:

nperseg is the segment length: it defaults to 256 if you don't set it
63,122 data points (126 seconds) in your data sample file: 63122/256=246 windows
246 windows will reduce the noise by sqrt(246)=15
The x-axis 80hz spur possible causes:
Your ODR looks to be set to 500hz: the 120v 60hz 7th harmonic (420hz) would alias down to 80hz.
Package resonance? try tapping on the engine block during data aquistion to see if the spur amplitude increases
Coupling from the SPI clock to the x-axis? try changing the SPI clock frequency
It's only one axis so probably not ground vibration, breezes, etc

--- Axis X Noise Characteristics ---
Valid Rows Parsed: 63122
Mean Static Offset: 0.9719 g
Total RMS AC Noise: 383.97 ug
RMS Noise Density: 24.28 ug/rtHz

--- Axis Y Noise Characteristics ---
Valid Rows Parsed: 63122
Mean Static Offset: -0.0605 g
Total RMS AC Noise: 405.82 ug
RMS Noise Density: 25.67 ug/rtHz

--- Axis Z Noise Characteristics ---
Valid Rows Parsed: 63122
Mean Static Offset: 0.0000 g
Total RMS AC Noise: 0.00 ug
RMS Noise Density: 0.00 ug/rtHz
 
Last edited:
... The FFT deals with sine and cosine which describe circles in the complex plane- that circle needs to be continuous for the math to work at it's best.
Probably off topic, but do you have a good way to conceptualize why multiplying a time-domain signal with Sine and Cosine translates it into the frequency domain? I still have a hard time really getting the concept. Still not very strong when something goes beyond simple math.
(My first job was at a place that also produced spectrum analyzers -- series of analog band-pass filters with their outputs displayed on an CRT screen.)
 
Probably off topic, but do you have a good way to conceptualize why multiplying a time-domain signal with Sine and Cosine translates it into the frequency domain? I still have a hard time really getting the concept. Still not very strong when something goes beyond simple math.
(My first job was at a place that also produced spectrum analyzers -- series of analog band-pass filters with their outputs displayed on an CRT screen.)

Sine and cosine are at right angles to each other: cos(0)=sin(90). That means they can define a two dimensional space just like the familiar xy axis defines a position on a two dimensional plane. The sampled data is multiplied by each bin frequency cosine, with the area of that result being the cosine magnitude. The same thing is done with the sine to get the sine magnitude. The sampled data magnitude at each frequency is sqrt(mag_sin**2+mag_cos**2) and the phase = arctan(mag_sin/mag_cos). Think of cosine as being the x axis and sine as being y axis, then "projecting" the sampled data on each axis to determine the relative strength of each. Another analogy is what is the "shadow" the sampled data projects on each sine and cosine axis. The net result is the DFT has three points for each pair of sine & cosine calculation: frequency, real, imaginary: the "real" part is the cosine magnitude, the "imaginary" part is sine magnitude. The real and imaginary axis are known as the "complex plane": it's just like the familiar xy axis but with different labels.
 
Last edited:
Sine and cosine are at right angles to each other: cos(0)=sin(90). That means they can define a two dimensional space just like the familiar xy axis defines a position on a two dimensional plane. The sampled data is multiplied by each bin frequency cosine, with the area of that result being the cosine magnitude. The same thing is done with the sine to get the sine magnitude. The sampled data magnitude at each frequency is sqrt(mag_sin**2+mag_cos**2) and the phase = arctan(mag_sin/mag_cos). Think of cosine as being the x axis and sine as being y axis, then "projecting" the sampled data on each axis to determine the relative strength of each. Another analogy is what is the "shadow" the sampled data projects on each sine and cosine axis. The net result is the DFT has three points for each pair of sine & cosine calculation: frequency, real, imaginary: the "real" part is the cosine magnitude, the "imaginary" part is sine magnitude. The real and imaginary axis are known as the "complex plane": it's just like the familiar xy axis but with different labels.
Thank you all. Looks like it boils down to not understanding why multiplying with Cos and Sin of a given frequency tends to zero out all other frequencies.
 
I love looking at the first few posts in this thread and how “easy it should be”, lol

As someone who codeveloped a precision MEMS-based three-axis accelerometer that has to measure 0-4Hz accelerations with milli-g accuracy in a out-of-band vibration environment of up to 30 g’s peak, let’s just say “it was a learning experience “.
Still in production almost twenty years later, so I guess we did okay.
Y’all have fun!
 
Thank you all. Looks like it boils down to not understanding why multiplying with Cos and Sin of a given frequency tends to zero out all other frequencies.

Very good, you are getting it! I was debating whether to go there and decided to leave the trig out of the discussion to avoid the complications you'll read below.
The trig identities are:
1. cos(a)*cos(b)=0.5*(cos(a+b)+cos(a-b))
2. sin(a)*sin(b)=0.5*(cos(a-b)-cos(a+b))
3. sin(a)*cos(b)=0.5*(sin(a+b)+sin(a-b))
Multiplying sines and cosines creates sum and difference frequencies: the a+b and a-b. This is super important in communications.
The area under the curve (called the integral) of the sine and cosine over integer multiples of 360 degrees is always zero. Equal parts above and below the x axis.
Let's say a is the frequency in the sampled data, and b is the bin frequency:
If b=a in equation 1: the area of 0.5*(cos(2a)+cos(0)) = 0.5
if b=2a in equation 1: the area of 0.5*(cos(3a)+cos(-a)) = 0
Now for the reason I didn't include this in the previous discussion:
if b=0.9a in equation 1: the area of 0.5*(cos(1.9a)+cos(0.1a))=a non zero value: it's called spectral leakage in the discrete Fourier transform.
Spectral leakage happens because the sampling window is finite which produces a "sinc" pulse with side lobes instead of a single impulse at frequency "a".
The continuous time Fourier transform is an integral that integrates from minus infinity to plus infinity, so those fractional frequencies would integrate to zero.
The DFT integration is too short to give an accurate result for fractional frequencies.
The key take away is the sum and difference frequencies allows bin frequency correlation to the data and that spectral leakage is caused by the DFT finite window length.
 
Last edited:
I love looking at the first few posts in this thread and how “easy it should be”, lol

As someone who codeveloped a precision MEMS-based three-axis accelerometer that has to measure 0-4Hz accelerations with milli-g accuracy in a out-of-band vibration environment of up to 30 g’s peak, let’s just say “it was a learning experience “.
Still in production almost twenty years later, so I guess we did okay.
Y’all have fun!

Awesome! So many questions I could ask. What were the "biggies" you learned?
 
Very good, you are getting it! I was debating whether to go there and decided to leave the trig out of the discussion to avoid the complications you'll read below.
The trig identities are:
1. cos(a)*cos(b)=0.5*(cos(a+b)+cos(a-b))
2. sin(a)*sin(b)=0.5*(cos(a-b)-cos(a+b))
3. sin(a)*cos(b)=0.5*(sin(a+b)+sin(a-b))
Multiplying sines and cosines creates sum and difference frequencies: the a+b and a-b. This is super important in communications.
The area under the curve (called the integral) of the sine and cosine over integer multiples of 360 degrees is always zero. Equal parts above and below the x axis.
Let's say a is the frequency in the sampled data, and b is the bin frequency:
If b=a in equation 1: the area of 0.5*(cos(2a)+cos(0)) = 0.5
if b=2a in equation 1: the area of 0.5*(cos(3a)+cos(-a)) = 0
Now for the reason I didn't include this in the previous discussion:
if b=0.9a in equation 1: the area of 0.5*(cos(1.9a)+cos(0.1a))=a non zero value: it's called spectral leakage in the discrete Fourier transform.
Spectral leakage happens because the sampling window is finite which produces a "sinc" pulse with side lobes instead of a single impulse at frequency "a".
The continuous time Fourier transform is an integral that integrates from minus infinity to plus infinity, so those fractional frequencies would integrate to zero.
The DFT integration is too short to give an accurate result for fractional frequencies.
The key take away is the sum and difference frequencies allows bin frequency correlation to the data and that spectral leakage is caused by the DFT finite window length.
Thanks -- I think. I guess I could work my way through this ...
But I guess I'm looking for more of a graphic representation that will get me a conceptional understanding. Like looking at the curve of a 1/3 octave band-pass filter.
Hard to admit that in spite of my education in electronics I never fully grasped or maybe never learned quadrature decoding and so on. I seem to vaguely remember working my way through understanding of FM decoders and even how stereo signals are extracted from an encoded FM signal but that was a l o n g time ago. If that is even related to subject at hand ...

EDIT: Not quadrature decoding. I was thinking about analogue multipliers in connection with decoding signals. Or maybe quadrature demodulation. I really don't remember learning about that back in the 70's.
 
Last edited:
Thanks -- I think. I guess I could work my way through this ...
But I guess I'm looking for more of a graphic representation that will get me a conceptional understanding. Like looking at the curve of a 1/3 octave band-pass filter.
Hard to admit that in spite of my education in electronics I never fully grasped or maybe never learned quadrature decoding and so on. I seem to vaguely remember working my way through understanding of FM decoders and even how stereo signals are extracted from an encoded FM signal but that was a l o n g time ago. If that is even related to subject at hand ...

EDIT: Not quadrature decoding. I was thinking about analogue multipliers in connection with decoding signals. Or maybe quadrature demodulation. I really don't remember learning about that back in the 70's.
Think of it this way- multiplying a cosine times a time domain signal will produce a DC value when that frequency is present, otherwise it's a zero DC value. Very simple and very powerful. I admire your hanging in there to understand it.

This python script will plot the result of multiplying two sine waves together after you input the two frequencies. You will be able to see the how the DC value shifts depending on the two frequencies.
 

Attachments

Last edited:
Found this (and another one):
Also thanks to CF86301 for the spreadsheet he provided.
By plotting the signal to be analyzed around a circle and also plotting the curve of a known frequency around the circle and multiplying the two at each plotted point, only those points where they coincide do they produce a significant value when summed up. There's probably a better explanation but that sort of explains the concept for me.

Edit: Whereas vector addition is a fairly easy concept to get, vector multiplication not so much. Thus the difficulty in fully (graphically) getting the concept of multiplying sample points on two frequency curves together, but intuitively must have to do with how close any two points are together and their magnitudes.
 
Last edited:
The key take away is the sum and difference frequencies allows bin frequency correlation to the data and that spectral leakage is caused by the DFT finite window length.

Obviously🤣

Who could have guessed I would ever find this discussion on the Vans Airforce forum.
 
Obviously🤣

Who could have guessed I would ever find this discussion on the Vans Airforce forum.
Chuckle! 😀 If it has to do with airplanes, there is an interest somewhere in the Van sub-universe. If you are flying a twin and the engines aren’t at the same RPM, you are hearing the difference frequency reminding you to synchronize the RPMs.
 
Chuckle! 😀 If it has to do with airplanes, there is an interest somewhere in the Van sub-universe.
This is a strange alternate sub-universe in which certain mechanical engineers are writing digital signal processing code, something that only a minority of electronics engineers do.
 
This is a strange alternate sub-universe in which certain mechanical engineers are writing digital signal processing code, something that only a minority of electronics engineers do.
ME's who dabble in code are awesome! I wish you'd (I'm assuming) rub off on some of my buddies who are MEs. :)
 
On my plots, g rms wasn't intended to represent any sort of statistical tool used to clean up the data. It was just the root mean square of the vibration amplitude coming out of the FFT. (For a perfect sine wave it would be 0.707 x peak).

I don't have any data collected with the accelerometer on the hangar floor, but I do have data collected with it mounted on the airplane inside the hangar on a calm day. The raw data is available here, in the spreadsheet named 'AirplaneOff 5/13'. Below is the FFT.
View attachment 118107
I don't know what's causing the ~80Hz spike on the X axis. I wasn't expecting to see that. Also be sure to the note the '1e-5' on the amplitude scale.


I'm no longer using the ADXL345, I have switched to the ADXL355. Coming into the eval board it's 8 mV. Going into the ADXL355 I'm measuring 2.6 mV.

Keep in mind that the data presented on this post is from my test setup. Other than the sensor, it's an entirely different setup from what the other's here are testing. This includes the plots you commented on just a few days ago. Those were from data provided by petehowell. So this data should be evaluated separately from that.
What is your ODR?
 
It's set for 2000. I'm measuring around 2016. I tried 4k, but the buffer requirements to handle the delays writing to the SD card were too much.
I had to go to 1000 ODR to eliminate the clipping. Getting the 355 to take and keep that command was a challenge. I'm getting better data now. I have been trying to balance at 2080 rpm, but the 2x data is really not good there. I took data at 2270 this morning and the 2x is much quieter there. I'll get data for an IC solution tomorrow if they dont dilate my eyes in the morning.

With a clean tach signal. no clipping, and quieter 2x - I'm thinking I have a shot.

Lies we tell ourselves........... Do Aero E's who have worked in healthcare for 37 years dabbling in DSP code count????
 
Last edited:
It's set for 2000. I'm measuring around 2016. I tried 4k, but the buffer requirements to handle the delays writing to the SD card were too much.
That's surprising there is a bottleneck at a 4khz ODR. Are you using a dedicated core to store the raw binary data using two 512 byte arrays as ping-pong buffers?
 
That's surprising there is a bottleneck at a 4khz ODR. Are you using a dedicated core to store the raw binary data using two 512 byte arrays as ping-pong buffers?
Core 0 is responsible for collecting accelerometer data and storing it in a buffer. Core 1 reads data from that buffer, formats it, and writes the data to the file system on the SD card. Core 1 is also responsible for all BLE communication, temperature reading/logging, all FFT calculations, handling debug commands, and communicating with the RTC. Normally there is plenty of overhead to do all of this. However, there are occasions when the SD card write stalls, and takes an abnormally long time to complete a data write. I'm assuming it must be performing some type of internal housekeeping (erasing a sector, wear leveling, etc.). If these delays occur at the same time as core 1 is already busy with multiple other tasks, we end up dropping some data. If it was a priority, I'm sure I could optimize things some more and get operation at 4k.

In a worst case test, I saw data get dropped 48 times over a 873 second (~14.5 minute) test. For reference, 873 seconds is nearly 3.5 million samples. Each accelerometer sample is 28 bytes of data (8 byte timestamp, 4 bytes X, 4 bytes Y, 4 bytes Z, and 8 bytes for last tach signal timestamp), and my buffer is sized to hold 2048 samples (56 kBytes). The SD card is using a FAT32 filesystem, and all data is written as text into a .csv file.
 
Core 0 is responsible for collecting accelerometer data and storing it in a buffer. Core 1 reads data from that buffer, formats it, and writes the data to the file system on the SD card. Core 1 is also responsible for all BLE communication, temperature reading/logging, all FFT calculations, handling debug commands, and communicating with the RTC. Normally there is plenty of overhead to do all of this. However, there are occasions when the SD card write stalls, and takes an abnormally long time to complete a data write. I'm assuming it must be performing some type of internal housekeeping (erasing a sector, wear leveling, etc.). If these delays occur at the same time as core 1 is already busy with multiple other tasks, we end up dropping some data. If it was a priority, I'm sure I could optimize things some more and get operation at 4k.

In a worst case test, I saw data get dropped 48 times over a 873 second (~14.5 minute) test. For reference, 873 seconds is nearly 3.5 million samples. Each accelerometer sample is 28 bytes of data (8 byte timestamp, 4 bytes X, 4 bytes Y, 4 bytes Z, and 8 bytes for last tach signal timestamp), and my buffer is sized to hold 2048 samples (56 kBytes). The SD card is using a FAT32 filesystem, and all data is written as text into a .csv file.
I hear you, there are so many things that can go wrong in the hardware/software chain. :)
 
Well, still no great solution, but a lot of data - publishing to see if those smarter than I might have some ideas. I'm using Claude to track the results and here is the write-up. Suggestions, corrections, and insults to my intellect are all appreciated!

I'll be the first to admit that I am in over my head here, but I'm having fun (the tower sometimes asks how the data looked). I'm also getting really good at removing and replacing the top cowl to change weights........

Thanks for taking a look!
 

Attachments

Well, still no great solution, but a lot of data - publishing to see if those smarter than I might have some ideas. I'm using Claude to track the results and here is the write-up. Suggestions, corrections, and insults to my intellect are all appreciated!

I'll be the first to admit that I am in over my head here, but I'm having fun (the tower sometimes asks how the data looked). I'm also getting really good at removing and replacing the top cowl to change weights........

Thanks for taking a look!
I had to go to 1000 ODR to eliminate the clipping.
To fix your clipping, did you make any physical modifications or just change up your ODR? In my reading of the datasheet, I'm not seeing anything that indicates just changing the ODR should fix clipping. It does change the sample rate of the internal ADC and modifies some of the filtering. However, it sounds like all of this is being performed after the sensor itself. So if the sensor is still clipping, you'll still be missing data. Even if the post sensor filtering is removing clipped values from the resulting output data.
 
Shooting in the dark here.
Section 3...
  1. Do we know that when the system spits out "90 degrees" that 90 degrees is the HEAVY SIDE? Some way to bench test and get confirmation?
  2. Flight A spits out 92 degrees. So why do we add a weight at 150 degrees, and not 270? 150 is closer to 90 than to 270, and the balance improves? Shouldn't it get worse?
We are missing something obvious... old legacy balancers couldn't have had this level of calculation (could they?).
Maybe they just did a (physical electronic) filter around 2000 rpm (about 33 Hz) to toss out all the other vibrations.

Crazy(?) thought --- try testing in a gliding descent. That should get all the combustion shake out of the picture. On the same flight with same weights, same rpm if possible power on and off.

Another thought... move a know weight around to 4 different holes each 90 degrees apart. One should show improvement, is it the one that makes sense?
 
Can any of you confirm that the ADXL355 always defaults to +-2Gs and 4,000HZ sample rate after power on?
I should be seeing 3 times the amount of samples that I'm actually seeing.
Yes, I require RPM to be in a pretty narrow RPM range (+-20) but that appears fairly easy to maintain at 1,200RPM on the ground and a 2.85:1 gearbox.
I'm also limiting to 1G, but there I'm counting rejected samples which amounts to 3% or less.

Anybody see anything obvious below (don't ask Claude, this is pretty much from Claude's review although simplified to hard X and Y 1G limits)?

// ===========================================================================
// loop() (core 1, priority 2)
// Runs at up to 4000 Hz driven by ADXL355 DRDY interrupt flag.
// Reads accelerometer, assigns sample to angular slot, applies outlier
// rejection, accumulates into slot buffers.
// ===========================================================================
void loop() {
if (!bDataReady) return; //bDataReady is set to true by ADXL355 DRDY interrupt
bDataReady = false;
ADXL355Data d;
accel.readData(d);
temp = d.temp_c; //get sensor temperature in celsius
// Atomically snapshot ISR-written timing variables
uint64_t local_period, local_last_tach, local_sample_time;
uint16_t local_rpm;
portENTER_CRITICAL(&mux);
local_period = period_us;
local_last_tach = last_tach_time_us;
local_sample_time = sample_time_us;
local_rpm = rpm;
portEXIT_CRITICAL(&mux);
// --- Gate checks --------------------------------------------------------
if (local_period == 0) return; // no tach yet
if (local_sample_time < local_last_tach) return; // sample before tach edge
//only accept samples that are within (narrow) RPM range because RPM is used to calculate ips
if (local_rpm < (TargetRPM - TargetRPMTolerance)) return;
if (local_rpm > (TargetRPM + TargetRPMTolerance)) return;
// --- Slot assignment ----------------------------------------------------
uint64_t delta = local_sample_time - local_last_tach;
if (delta >= local_period) return; // wrapped past one rev
int index = (int)((SAMPLE_SLOTS * delta) / local_period);
if (index < 0 || index >= SAMPLE_SLOTS) return; // safety clamp
// --- Outlier rejection --------------------------------------------------
if (abs(d.x) > 256000) {sample_buffer[index].rejected += 1; return;} //limit to 1Gs
if (abs(d.y) > 256000) {sample_buffer[index].rejected += 1; return;}
sample_buffer[index].NumberOfSamples += 1;
sample_buffer[index].x += d.x;
sample_buffer[index].y += d.y;
}

EDIT: Should mention that until a few days ago I was hard limiting to 0.25Gs but found that increasing that to 1G made it converge to lower IPS. Also more slots makes for lower IPS. Go figure. Beginning to realize that I'm groping in the dark. Same issues as Pete's: adding weights do not produce expected results. Afraid to go fly again because of the apparent high IPS (0.7ips @1200 RPM) on the ground, but now beginning to wonder if they are true.
1780018609764.png
 
Last edited:
Pete, from an earlier picture it appears you have wires running from the ADXL355 to the cockpit?
How long are your wires from the ADXL355 to your uProcessor?
Which SPI clock speed are you running at? 5MHz?
Because it appears that I'm groping in the dark and because BLE is not designed for fast large data transfers (and because it gets hot out there) I'm beginning to think I need to move the ESP32 into the cockpit, add display and SD card so I can display spectrum and log raw sample data instead of just displaying a summary on my phone.
 
Pete, from an earlier picture it appears you have wires running from the ADXL355 to the cockpit?
How long are your wires from the ADXL355 to your uProcessor?
Which SPI clock speed are you running at? 5MHz?
Because it appears that I'm groping in the dark and because BLE is not designed for fast large data transfers (and because it gets hot out there) I'm beginning to think I need to move the ESP32 into the cockpit, add display and SD card so I can display spectrum and log raw sample data instead of just displaying a summary on my phone.
Hi Finn,

I do have wires shielded wires running into the cockpit - they are about 6 ft long. I’m running at 1Mhz . It does seem speed is limited by the length of wires. I think I could go faster, but I dont need to.
 
To fix your clipping, did you make any physical modifications or just change up your ODR? In my reading of the datasheet, I'm not seeing anything that indicates just changing the ODR should fix clipping. It does change the sample rate of the internal ADC and modifies some of the filtering. However, it sounds like all of this is being performed after the sensor itself. So if the sensor is still clipping, you'll still be missing data. Even if the post sensor filtering is removing clipped values from the resulting output data.
Hi - after some searching and chatting when made the chnage - here is my understanding, and it seems to make some sense. Do I completely understand - not a chance..... The deep dive does have me wondering if a ADXL357 might be a better sensor for us with 40g limits. Please call BS on any of this -

The ADXL355's internal LPF cutoff is tied to ODR — it's not a separate setting:

ODR Internal LPF corner

  • 4000 Hz 1000 Hz
  • 1000 Hz 250 Hz
At 4000 Hz ODR, the output register contains the full wideband signal up to 1000 Hz. Your prop/engine structure has resonances — blade pass harmonics, mount modes, structural ring frequencies — scattered across that band. None of them individually clips the ADC, but their instantaneous peaks sum. When several resonances hit their peaks simultaneously, the combined acceleration can exceed the ADXL355's full-scale range (±2g, ±4g, or ±8g depending on your RANGE register setting) and the 20-bit output register saturates.

At 1000 Hz ODR, the internal decimation filter rolls off at 250 Hz. Everything above 250 Hz — all those high-frequency structural resonances — is attenuated before it ever reaches the output register. What's left is essentially just the low-frequency prop imbalance fundamental (~38 Hz at cruise) and its first few harmonics, which are well within range.

The ODR fix worked because we didn't need that high-frequency content anyway — our measurement target is the 1x shaft frequency and its harmonics up to maybe 4x or 5x, all well below 250 Hz even at max RPM. The 4000 Hz ODR was buying you nothing useful while exposing you to wideband clipping.


I do grab raw data snapshots and had claude take a look at a few at 1000ODR:

File 1736 (y_raw_g peaks):
  • Max positive: +3.85g (seq 1423, theta=8.8°)
  • Max negative: -3.92g (seq 1388, theta=259.8°)
File 1742 (y_raw_g peaks):
  • Max positive: +4.11g (seq 133, theta=123.6°)
  • Max negative: -4.27g (seq 525, theta=260.3°)
No clipping. Threshold is 8.19g — you're at 50% of full scale in the worst file. Clean headroom.

What else the data shows:

The large peaks don't cluster at a consistent theta angle. In file 1736, big positives appear at 8°, 26°, 118°, 200°, 248°, 337° — scattered across the full revolution. That's classic multi-harmonic summation, not a single dominant frequency. When 2x, 4x, and combustion harmonics happen to align constructively, you get a short-duration spike.

y_filt vs y_raw: The notch filter is making small differences — sometimes 0.2–0.4g reduction at the peaks, occasionally y_filt slightly exceeds y_raw by a similar margin (normal IIR phase-shift behavior, not a concern). The 0.5x notch isn't the dominant driver of the broadband envelope.

Bottom line: The sensor mount is fine, the range is appropriate, and these files don't indicate any hardware issue. The ±4g broadband is normal for a direct-mounted ADXL355 on a Lycoming at cruise — it's engine harmonics within the 250 Hz passband, and the TSVA integrates them away.
 
Back
Top