VC31 explanation

Espruino recently added a blob from the manufacturer of the HRM sensor, VCare, to improve its accuracy. This post will try to explain the Algo_Input function.

Decompilation

void Algo_Input(int* param_1, int param_2, int param_3, int param_4)

{
  undefined4 uVar1;
  int iVar2;
  int iVar3;
  int iVar4;
  int iVar5;
  int iVar6;
  uint uVar7;
  uint uVar8;
  int iVar9;
  uint uVar10;
  bool bVar11;
  bool bVar12;
  undefined4 uVar13;
  int* piVar14;
  int iVar15;
  undefined4 local_c0[3];
  int local_b4;
  undefined auStack_ac[96];
  undefined4 local_4c;
  undefined4 local_48[2];
  int local_40;
  int local_3c;
  undefined4 local_38;
  int* piStack_34;
  int iStack_30;
  int iStack_2c;
  int local_28;

  local_4c = 0;
  local_48[0] = 0;
  local_40 = 0;
  local_3c = 0;
  isBusy = 1;
  uVar8 = 0;
  uVar10 = 0;
  interTime = interTime + param_2;
  iVar2 = interCnt + 1;
  if (param_2 > 60) {
    iVar2 = interCnt;
  }
  interCnt = iVar2;
  piStack_34 = param_1;
  iStack_30 = param_2;
  iStack_2c = param_3;
  local_28 = param_4;
  if (interTime > 970) {
    uVar8 = 25 - interCnt;
    if ((int)uVar8 < 0) {
      uVar8 = 0;
    }
    interCnt = 0;
    interTime = 0;
    if ((int)uVar8 < 18) {
      if ((int)uVar8 < 1)
        goto label_6;
    } else {
      uVar8 = 18;
    }
    uVar10 = 1;
  }
  do {
    if (uVar8 == 0 || uVar10 == 0) {
      preRawdata._12_4_ = param_1[3];
      preRawdata._16_4_ = param_1[4];
      preRawdata._0_4_ = *param_1;
      preRawdata._4_4_ = param_1[1];
      preRawdata._8_4_ = param_1[2];
      if (param_3 == 18) {
        preRawdata._0_4_ = preRawdata._0_4_ << 2;
        preRawdata._4_4_ = preRawdata._4_4_ << 2;
        preRawdata._8_4_ = preRawdata._8_4_ << 2;
      }
    }
    if ((uVar8 == 0) && (uVar10 != 0)) {
      preRawdata._12_4_ = preRawdata._12_4_ + 4096;
    }
    modle5_2(preRawdata, local_c0, &local_3c);
    iVar2 = local_b4;
    if (preAccnorm == 0) {
      preAccnorm = local_3c;
    }
    iVar9 = local_3c - preAccnorm;
    bus = iVar9 * 16;
    if (bus < 0) {
      bus = iVar9 * -16;
    }
    prePPGUC = prePPGUC + ((bus - prePPGUC) * 261 + 16384 >> 15);
    if (prePPGUC > 255) {
      prePPGUC = 256;
    }
    if (prePPGUC < -255) {
      prePPGUC = -256;
    }
    if (iVar9 < 0) {
      iVar9 = -iVar9;
    }
    preAccnorm = local_3c;
    if (accGapMax <= iVar9) {
      accGapMax = iVar9;
    }
    if (param_3 == 1) {
      param_3 = 2;
    label_1:
      if (prePPGAfterFilter == 0) {
        prePPGAfterFilter = local_b4;
      }
      iVar3 = prePPGAfterFilter - local_b4;
      if (iVar3 < 0) {
        iVar3 = -iVar3;
      }
      if (iVar3 > 4799) {
        ppgPower25Hz = local_b4;
        local_b4 = prePPGAfterFilter;
      }
      prePPGAfterFilter = iVar2;
    } else if (param_3 == 2)
      goto label_1;
    uVar1 = modle5_11(local_b4);
    PPGpowerReal = modle5_10(local_b4);
    modle5_17(local_c0, param_3, iVar9);
    iVar2 = modle5_1();
    iVar9 = modle5_9(preRawdata);
    iVar3 = modle5_18(preRawdata, local_c0[0]);
    local_38 = modle5_7();
    iVar4 = modle5_3(iVar3);
    bigbus = iVar4;
    modle5_13(iVar2 << 4, RLCGroup, auStack_ac);
    oneSecondCnt = oneSecondCnt + '';
    if (prePPGUC < 96) {
      absoluteStillCnt = absoluteStillCnt + 1;
    } else {
      absoluteStillCnt = 0;
    }
    if (local_28 == 0) {
      absoluteStillCnt = 0;
    } else if (199 < absoluteStillCnt) {
      absoluteStillCnt = 200;
    }
    if ('' < oneSecondCnt) {
      oneSecondCnt = '';
      rlcSwitchCnt = rlcSwitchCnt + 1;
      modle5_15(auStack_ac, lockOn, firstLock, (undefined4)enhanceRange,
                enhanceRange._4_4_, (undefined4)dampingRange,
                dampingRange._4_4_);
      preRawheartRate = heartRate;
      preRawReliability = reliability;
      modle5_14(auStack_ac, &local_4c, &heartRate, local_48);
      reliability =
          modle5_8(auStack_ac, local_48[0], local_4c, iVar9, &PPGpowerFromSpec);
      if ((PPGpowerReal < 0) && (firstLock == '')) {
        reliability = 0;
      }
      uVar7 = (uint)(absoluteStillCnt == 200);
      if (runningAlltime < 30) {
        runningAlltime = runningAlltime + 1;
      }
      iVar5 = preRawReliability - reliability;
      iVar2 = iVar5;
      if (iVar5 < 0) {
        iVar2 = -iVar5;
      }
      if ((iVar2 * 100 + 128 >> 8 < 5) &&
          (79 < (reliability * 100 + 128) >> 8)) {
        signalSafeGuardCnt = signalSafeGuardCnt + 1;
        if (4 < signalSafeGuardCnt) {
          absoluteStillCnt = 0;
          uVar7 = 0;
          signalSafeGuardCnt = 5;
        }
      } else {
        signalSafeGuardCnt = 0;
      }
      if (((firstLock == '') && (15 < iVar5 * 100 + 128 >> 8)) &&
          (runningAlltime == 30)) {
        ProtectTic = 5;
        if ((param_3 == 0) && (local_40 == 1)) {
          ProtectTic = 300;
        }
      label_2:
        ProtectTic = ProtectTic + -1;
      } else if (ProtectTic != 0)
        goto label_2;
      iVar2 = preRawheartRate - heartRate;
      if (param_3 == 0) {
        if (ProtectTic == 0) {
          if (iVar2 + 12800 < 0 != SCARRY4(iVar2, 12800)) {
            bVar12 = SBORROW4(heartRate, 35840);
            iVar5 = heartRate + -35840;
            bVar11 = heartRate == 35840;
            if (heartRate > 35840)
              goto label_3;
          }
        } else if (iVar2 < 1) {
          if (preRawheartRate < 23041) {
            if (iVar2 + 2560 < 1280) {
              heartRate = preRawheartRate - iVar2 / 2;
            }
            if (iVar2 + 3840 < 1280) {
              heartRate = preRawheartRate + 512;
            }
            if (iVar2 + 3840 < 0 != SCARRY4(iVar2, 3840)) {
              heartRate = preRawheartRate;
            }
          }
          if (preRawheartRate - 23040U < 5121) {
            if (iVar2 + 2560U < 1280) {
              heartRate = preRawheartRate + 256;
            }
            if (iVar2 + 2560 < 0 != SCARRY4(iVar2, 2560)) {
              heartRate = preRawheartRate;
            }
          }
          if (28160 < preRawheartRate) {
            if (iVar2 + 3840U < 1280) {
              heartRate = preRawheartRate + 512;
            }
            if (iVar2 + 3840 < 0 != SCARRY4(iVar2, 3840))
              goto label_4;
          }
        } else if (preRawheartRate < 28161) {
          if (iVar2 - 2561U < 1280) {
            heartRate = preRawheartRate + -1280;
          }
          bVar12 = SBORROW4(iVar2, 3840);
          iVar5 = iVar2 + -3840;
          bVar11 = iVar2 == 3840;
        label_3:
          if (!bVar11 && iVar5 < 0 == bVar12)
            goto label_4;
        }
      } else if (ProtectTic != 0) {
        if ((iVar2 + 7680 < 0 != SCARRY4(iVar2, 7680)) &&
            (heartRate > 40960)) {
          heartRate = preRawheartRate;
        }
        if ((iVar2 > 5120) && (heartRate < 16640)) {
        label_4:
          heartRate = preRawheartRate;
        }
      }
      piVar14 = &local_40;
      iVar3 = iVar3 >> 4;
      if (iVar9 != 0) {
        iVar9 = 1;
      }
      uVar13 = local_38;
      iVar2 = iVar4;
      iVar5 = preHR;
      iVar15 = param_3;
      modle5_5(&HRlast);
      if (0 < badSignalCnt) {
        badSignalCnt = badSignalCnt + -1;
      }
      iVar6 = HRlast;
      if (accGapMax < 60000) {
        if ((0 < badSignalCnt) || ((ProtectTic != 0 && (param_3 == 0))))
          goto label_5;
      } else {
        badSignalCnt = 30;
      label_5:
        if (HRpatch != 0) {
          if (HRlast - HRpatch < 1280) {
            iVar6 = HRpatch + ((HRlast - HRpatch) * 405 >> 13);
          } else {
            iVar6 = HRpatch + 63;
          }
        }
      }
      HRpatch = iVar6;
      if ((iVar6 - preHR) + 256U < 513) {
        HRpatch = preHR;
      }
      if ((iVar4 < 81) && (37119 < HRlast)) {
        if (abnormalHighHRCnt < 30) {
          abnormalHighHRCnt = abnormalHighHRCnt + 1;
        }
      } else {
        abnormalHighHRCnt = 0;
      }
      if (firstLock != '') {
        param_3 = 1;
      }
      if (abnormalHighHRCnt == 30) {
        HRlast = HRlast >> 1;
        param_3 = 0;
      }
      preHR = HRpatch;
      if (rlcSwitchCnt == 1) {
        modle5_4(HRlast + 128, firstLock, iVar4, RLCGroup, local_40, param_3,
                  1, uVar13, iVar9, iVar3, uVar1, iVar2, iVar5, piVar14, iVar15,
                  uVar7);
        rlcSwitchCnt = 0;
      }
      if (firstLock != '') {
        iVar2 = 0;
        do {
          RLCGroup[iVar2] = 1;
          iVar2 = iVar2 + 1;
        } while (iVar2 < 12);
      }
      accGapMax = 0;
    }
    uVar8 = uVar8 - 1;
  label_6:
    if ((int)uVar8 < 0) {
      isBusy = 0;
      return;
    }
  } while (true);
}

Source: Ghidra (Dogbolt) + http://format.krzaq.cc + unhexing by me

Explanation by GPT-4

(This function is quite complex without a concrete context and uses many variables and functions that are not defined within its scope. I will explain as much as possible based on the variables and logic provided within the function itself.)

The input to the function Algo_Input is four parameters.

This function appears to be part of a heart rate monitoring system, with some algorithm calculating beats per minute (BPM) from raw input data.

The function first does some initial calculation and preparation using param_2 and param_3. These likely represent some raw data inputs. These values are used to calculate an interTime and interCnt, which might represent some type of intermediate calculation.

A major part of the function seems to be a loop, probably processing a batch of new heart rate data. The function seems to be in readiness to calculate bpm even if the interTime value is greater than a threshold (970), which is probably some time duration.

Within this loop, the function performs preprocessing operations on raw input, via operations like shifting or adding depending on param_3. Then various methods are called that handle the data further, likely filtering and normalising it. These are likely correlated to heart rate signal processing methods which are commonly used in heart rate sensor algorithms. Variables like preAccnorm, bus, prePPGUC, etc are used, to indicate these might be values linked to accelerometer data and PPG (photoplethysmography) sensor data.

There is seemingly an accumulative process where the cumulative sum of absoluteStillCnt is computed whenever param_3 is nonzero, which may indicate the device being still. This implies the algorithm recognizes user's motion state based on the acceleration norm value provided in param_1 and modifies its behavior accordingly.

There are also several operations checking and adjusting the heart rate value (heartRate) if certain conditions apply. The measures of reliability are considered and safety checks on the signal are in place (signalSafeGuardCnt), ensuring the data's integrity isn't compromised.

Towards the end of the function, adjustments are made to the final heart rate (HR) output according to certain conditions (HRpatch, HRlast), for example, abnormal HR is detected and adjusted by halving the HRlast.

Overall, this code seems to be a mix of processes related to signal processing, safety checks, and adjustments based on real-time information and measurements, adapting accordingly.

Disclaimer: This is a qualified guess based on the provided code. Some parts may not be accurate due to a lack of specific details and context.

Explanation by Gemini 2.5 Pro

The Algo_Input function serves as the main data processing entry point for the VC31 heart rate algorithm blob. It's called repeatedly with batches of raw sensor data (PPG and likely 3-axis Accelerometer) and the time elapsed since the last call.

Core Functionality:

  1. Data Ingestion & Timekeeping: It takes the raw sensor array (param_1), time delta (param_2), and mode flags (param_3, param_4). It maintains internal time counters and handles potential time gaps by running processing loops iteratively to "catch up" if needed.
  2. Per-Sample Processing: For each sample (or interpolated sample during catch-up), it extracts features:
    • Calculates the accelerometer vector magnitude (norm).
    • Derives a motion metric based on the change in ACC norm (likely smoothed with an IIR filter).
    • Applies initial filtering to the raw PPG signal(s).
  3. ~1Hz Processing Block: Roughly once per second, it performs the main HR calculation:
    • Spectral Analysis: It likely uses accumulated PPG data from the past second to perform spectral analysis (FFT or autocorrelation via internal modle5_x functions) to find the dominant frequency corresponding to the heart rate.
    • HR & Reliability Estimation: Extracts the raw heart rate estimate from the spectral peak and calculates a confidence/reliability score based on signal quality metrics (peak strength, noise levels, consistency).
    • Motion-Aware Smoothing: Applies significant stateful smoothing and filtering to the raw HR estimate. This stage heavily considers the calculated motion metrics, signal reliability trends, operating mode (param_3), and recent HR history to prevent erratic outputs, especially during motion or low signal quality periods. It uses mechanisms like ProtectTic to temporarily enforce stronger smoothing after stability disruptions.
    • Adaptive Feedback: The calculated HR, reliability, and motion status are fed back into the algorithm's internal state (modle5_4), likely tuning filter parameters, frequency tracking ranges (RLCGroup), or other coefficients for the next processing cycle.
  4. State Management: The function relies heavily on static/global variables to maintain context (previous sensor values, filtered signals, HR history, motion counters, lock status, etc.) between calls.

Key Technical Concepts Employed:

In essence, it's a classic embedded HRM pipeline: ingest raw data, filter/preprocess, use spectral methods to find HR frequency, assess signal quality, apply motion-aware smoothing, and adapt internal parameters over time. The complexity lies within the proprietary modle5_x functions implementing the core signal processing math and the intricate state-based logic for smoothing and adaptation.