AnasSarkiz/usb-c-pd-brushed-motor

USB-C PD-powered motor controller with protected input, selectable 5/9/12 V buck supply, MCU qualification, PWM H-bridge drive, overvoltage sensing, regenerative energy-dump resistors, and safety interlocks.

Version
0.0.1-a48-cloud-pipeline9-unrouted-c41d79a-305b1e7a
License
unset
Stars
0

firmware/pd_sequence.c

#include "pd_sequence.h"

/* Proposed timing/ADC windows require prototype verification. These timings
 * govern power/voltage changes only; no direction/reversal timer is introduced. */
#define SAMPLE_MAX_AGE_MS 5u
#define DECAY_TIMEOUT_MS 3000u /* 1k bleeder, <=650uF design envelope. */
#define FEEDBACK_SETTLE_MS 5u
#define CONTRACT_TIMEOUT_MS 2000u
#define RAIL_TIMEOUT_MS 200u
#define RAIL_STABLE_MS 10u
#define BRIDGE_WAKE_MS 2u /* TI tWAKE max 1 ms; review actual scheduling margin. */
#define DECAY_THRESHOLD_MV 1000u

void pd_sequence_abort(struct pd_sequence *sequence) {
  sequence->state = PD_SEQUENCE_FAULT;
  sequence->outputs.host_allow = false;
  sequence->outputs.release_bridge = false;
  sequence->outputs.request_contract = false;
  /* Preserve the existing feedback branch while VM may retain motor energy. */
}

static void enter_state(struct pd_sequence *sequence,
                        const struct pd_sequence_sample *sample) {
  sequence->entered_ms = sample->now_ms;
  sequence->rail_stable = false;
}

bool pd_sequence_begin(struct pd_sequence *sequence,
                       const struct pd_sequence_request *request) {
  if (!sequence || !request ||
      (sequence->state != PD_SEQUENCE_IDLE && sequence->state != PD_SEQUENCE_FAULT) ||
      sequence->outputs.host_allow || sequence->outputs.release_bridge ||
      sequence->outputs.request_id == UINT32_MAX || !pd_plan_valid(&request->plan)) return false;
  sequence->plan = request->plan;
  sequence->outputs.request_id++;
  sequence->outputs.request_contract = false;
  sequence->state = PD_SEQUENCE_DECAY;
  sequence->entered_ms = request->now_ms;
  sequence->last_step_ms = request->now_ms;
  sequence->rail_stable = false;
  return true;
}

void pd_sequence_step(struct pd_sequence *sequence,
                      const struct pd_sequence_sample *sample) {
  if (!sequence || !sample) return;
  if (sequence->state == PD_SEQUENCE_IDLE || sequence->state == PD_SEQUENCE_FAULT)
    return;
  const struct pd_observation *observation = &sample->observation;
  const struct pd_rail_check rail_check = {
    .plan=&sequence->plan, .observation=observation, .selector_bits=sample->selector_bits
  };
  if (!sample->adc_valid || sample->hard_reset || !observation->attached ||
      !observation->communication_ok ||
      !pd_voltage_interval_valid(&observation->vbus) ||
      !pd_voltage_interval_valid(&observation->motor_rail) ||
      observation->source_generation != sequence->plan.source_generation ||
      pd_motor_voltage(sample->selector_bits) != sequence->plan.motor_voltage_v ||
      sample->now_ms - sample->adc_sample_ms > SAMPLE_MAX_AGE_MS ||
      sample->now_ms - sequence->last_step_ms > SAMPLE_MAX_AGE_MS) {
    pd_sequence_abort(sequence);
    return;
  }
  sequence->last_step_ms = sample->now_ms;
  const uint32_t elapsed_ms = sample->now_ms - sequence->entered_ms;
  switch (sequence->state) {
  case PD_SEQUENCE_DECAY:
    if (elapsed_ms >= DECAY_TIMEOUT_MS) {
      pd_sequence_abort(sequence);
    } else if (observation->motor_rail.upper_mv <= DECAY_THRESHOLD_MV) {
      sequence->outputs.drive_9v = sequence->plan.motor_voltage_v == 9;
      sequence->outputs.drive_12v = sequence->plan.motor_voltage_v == 12;
      sequence->state = PD_SEQUENCE_FEEDBACK;
      enter_state(sequence, sample);
    }
    break;
  case PD_SEQUENCE_FEEDBACK:
    if (observation->motor_rail.upper_mv > DECAY_THRESHOLD_MV) {
      pd_sequence_abort(sequence);
    } else if (elapsed_ms >= FEEDBACK_SETTLE_MS) {
      sequence->outputs.request_contract = true;
      sequence->state = PD_SEQUENCE_REQUEST;
      enter_state(sequence, sample);
    }
    break;
  case PD_SEQUENCE_REQUEST:
    if (observation->motor_rail.upper_mv > DECAY_THRESHOLD_MV ||
        elapsed_ms >= CONTRACT_TIMEOUT_MS) {
      pd_sequence_abort(sequence);
    } else if (sample->completed_request_id == sequence->outputs.request_id) {
      sequence->outputs.request_contract = false;
      sequence->state = PD_SEQUENCE_CONTRACT;
      enter_state(sequence, sample);
    }
    break;
  case PD_SEQUENCE_CONTRACT:
    if (observation->motor_rail.upper_mv > DECAY_THRESHOLD_MV ||
        elapsed_ms >= CONTRACT_TIMEOUT_MS) {
      pd_sequence_abort(sequence);
    } else if (sample->ps_rdy_request_id == sequence->outputs.request_id &&
               pd_contract_qualified(&sequence->plan, observation)) {
      sequence->outputs.host_allow = true;
      sequence->state = PD_SEQUENCE_RAIL;
      enter_state(sequence, sample);
    }
    break;
  case PD_SEQUENCE_RAIL:
  case PD_SEQUENCE_WAKE:
  case PD_SEQUENCE_READY:
    if (sample->ps_rdy_request_id != sequence->outputs.request_id ||
        !pd_contract_qualified(&sequence->plan, observation) ||
        (uint64_t)observation->motor_rail.upper_mv * 100u >
            (uint64_t)sequence->plan.motor_voltage_v * 1000u * 105u) {
      pd_sequence_abort(sequence);
      break;
    }
    if (sequence->state == PD_SEQUENCE_RAIL) {
      if (elapsed_ms >= RAIL_TIMEOUT_MS) {
        pd_sequence_abort(sequence);
      } else if (pd_motor_qualified(&rail_check)) {
        if (!sequence->rail_stable) {
          sequence->rail_stable = true;
          sequence->rail_stable_since_ms = sample->now_ms;
        } else if (sample->now_ms - sequence->rail_stable_since_ms >= RAIL_STABLE_MS) {
          sequence->outputs.release_bridge = true;
          sequence->state = PD_SEQUENCE_WAKE;
          enter_state(sequence, sample);
        }
      } else {
        sequence->rail_stable = false;
      }
    } else if (!pd_motor_rail_qualified(&rail_check)) {
      pd_sequence_abort(sequence);
    } else if (sequence->state == PD_SEQUENCE_WAKE && elapsed_ms < BRIDGE_WAKE_MS) {
      /* nFAULT can pulse low at wake (TI Fig28). Hardware UVLO/OCP/TSD
       * remain active; only the bounded software fault decision is delayed. */
    } else if (observation->motor_fault) {
      pd_sequence_abort(sequence);
    } else {
      sequence->state = PD_SEQUENCE_READY;
    }
    break;
  default:
    pd_sequence_abort(sequence);
    break;
  }
}