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_test.c

#include "pd_sequence.h"
#include <assert.h>
#include <stdio.h>

/* Synthetic exact observations for existing transport/state regressions;
 * these are not generated by a production raw-ADC adapter. */
static struct pd_voltage_interval exact(uint32_t mv) {
  return (struct pd_voltage_interval){.lower_mv=mv,.upper_mv=mv,.valid=true};
}
static unsigned checks;
#define CHECK(condition) do { ++checks; assert(condition); } while (0)

static void tick(struct pd_sequence *sequence, struct pd_sequence_sample *sample) {
  sample->adc_sample_ms = sample->now_ms;
  pd_sequence_step(sequence, sample);
  sample->now_ms++;
}

static void make_ready(struct pd_sequence *sequence, struct pd_sequence_sample *sample) {
  const uint32_t source_pdos[] = {(100u << 10) | 300u, (300u << 10) | 300u,
                                 (400u << 10) | 300u};
  const struct pd_sequence_request request = {
    .plan=pd_make_plan(sample->selector_bits, &(struct pd_capabilities){.source_pdos=source_pdos, .count=3, .source_generation=1}), .now_ms=0
  };
  *sequence = (struct pd_sequence){0};
  sample->now_ms = 0;
  sample->adc_valid = true;
  sample->observation = (struct pd_observation){
    .attached=true, .communication_ok=true, .fresh_ps_rdy=true,
    .pe_fsm_state=0x18, .source_generation=1,
    .rdo=((uint32_t)request.plan.source_object_position << 28) | (300u << 10) | 300u,
    .vbus=exact(request.plan.voltage_mv), .motor_rail=exact(5000), .motor_fault=true
  };
  CHECK(pd_sequence_begin(sequence, &request));
  for (unsigned i=0; i<5; ++i) tick(sequence, sample);
  CHECK(sequence->state == PD_SEQUENCE_DECAY);
  CHECK(!sequence->outputs.host_allow && !sequence->outputs.release_bridge);
  CHECK(!sequence->outputs.drive_9v && !sequence->outputs.drive_12v);
  sample->observation.motor_rail=exact(0);
  tick(sequence, sample);
  CHECK(sequence->state == PD_SEQUENCE_FEEDBACK);
  CHECK(sequence->outputs.drive_9v == (sample->selector_bits == 1));
  CHECK(sequence->outputs.drive_12v == (sample->selector_bits == 2));
  for (unsigned i=0; i<6; ++i) tick(sequence, sample);
  CHECK(sequence->state == PD_SEQUENCE_REQUEST && sequence->outputs.request_contract);
  CHECK(!sequence->outputs.host_allow);
  sample->completed_request_id = sequence->outputs.request_id;
  tick(sequence, sample);
  CHECK(sequence->state == PD_SEQUENCE_CONTRACT);
  /* A PS_RDY from another request must not enable power. */
  sample->ps_rdy_request_id = 0;
  tick(sequence, sample);
  CHECK(!sequence->outputs.host_allow);
  sample->ps_rdy_request_id = sequence->outputs.request_id;
  tick(sequence, sample);
  CHECK(sequence->state == PD_SEQUENCE_RAIL && sequence->outputs.host_allow);
  CHECK(!sequence->outputs.release_bridge); /* UVLO fault must not deadlock power-up. */
  sample->observation.motor_rail=exact(request.plan.motor_voltage_v * 1000u);
  sample->observation.motor_fault = false;
  for (unsigned i=0; i<11; ++i) tick(sequence, sample);
  CHECK(sequence->state == PD_SEQUENCE_WAKE && sequence->outputs.release_bridge);
  sample->observation.motor_fault = true; /* TI Fig28 transient at wake. */
  tick(sequence, sample);
  CHECK(sequence->state == PD_SEQUENCE_WAKE && sequence->outputs.release_bridge);
  sample->observation.motor_fault = false;
  tick(sequence, sample);
  CHECK(sequence->state == PD_SEQUENCE_READY && sequence->outputs.release_bridge);
}

static void check_slow_discharge(uint8_t bits) {
  const uint32_t source_pdos[] = {(100u<<10)|300u, (300u<<10)|300u, (400u<<10)|300u};
  struct pd_sequence sequence = {0};
  const struct pd_sequence_request request = {.plan=pd_make_plan(bits, &(struct pd_capabilities){.source_pdos=source_pdos, .count=3, .source_generation=1})};
  struct pd_sequence_sample sample = {.selector_bits=bits, .adc_valid=true, .observation={
    .attached=true, .communication_ok=true, .source_generation=1, .vbus=exact(5000), .motor_rail=exact(15000), .motor_fault=true
  }};
  CHECK(pd_sequence_begin(&sequence, &request));
  /* 650uF, 1060ohm screen. Euler's 1ms step is only a host load model;
   * qualification uses the independently supplied VM observation, not a timer. */
  double rail_mv = 15000;
  while (sequence.state == PD_SEQUENCE_DECAY && sample.now_ms < 3000) {
    sample.observation.motor_rail=exact((uint16_t)(rail_mv + 1));
    tick(&sequence, &sample);
    CHECK(!sequence.outputs.host_allow && !sequence.outputs.release_bridge);
    if (sample.observation.motor_rail.upper_mv > 1000)
      CHECK(!sequence.outputs.drive_9v && !sequence.outputs.drive_12v);
    rail_mv *= 1.0 - 1.0 / (1060 * 650e-6 * 1000);
  }
  CHECK(sample.now_ms > 1000 && sample.now_ms < 3000);
  CHECK(sequence.state == PD_SEQUENCE_FEEDBACK);
  CHECK(sequence.outputs.drive_9v == (bits==1));
  CHECK(sequence.outputs.drive_12v == (bits==2));
}

static void check_ready_five_amp_source(uint8_t bits) {
  const uint32_t source_pdos[]={(100u<<10)|300u,(400u<<10)|500u};
  const struct pd_capabilities capabilities={.source_pdos=source_pdos,.count=2,.source_generation=77};
  const struct pd_sequence_request request={.plan=pd_make_plan(bits,&capabilities)};
  struct pd_sequence sequence={0};
  struct pd_sequence_sample sample={.selector_bits=bits,.adc_valid=true,.observation={
    .attached=true,.communication_ok=true,.fresh_ps_rdy=true,.pe_fsm_state=0x18,
    .source_generation=77,.vbus=exact(20000),.motor_rail=exact(0),.rdo=(2u<<28)|(300u<<10)|500u,.motor_fault=true
  }};
  CHECK(pd_sequence_begin(&sequence,&request));
  for (unsigned ms=0;ms<40;++ms) {
    if (sequence.state == PD_SEQUENCE_REQUEST)
      sample.completed_request_id=sequence.outputs.request_id;
    if (sequence.state == PD_SEQUENCE_CONTRACT)
      sample.ps_rdy_request_id=sequence.outputs.request_id;
    if (sequence.outputs.host_allow) {
      sample.observation.motor_rail=exact(request.plan.motor_voltage_v*1000u);
      sample.observation.motor_fault=false;
    }
    tick(&sequence,&sample);
  }
  CHECK(sequence.state == PD_SEQUENCE_READY && sequence.outputs.host_allow && sequence.outputs.release_bridge);
  CHECK(sequence.plan.current_ma == 3000 && (sequence.plan.source_pdo & 1023u) == 500u);
  sample.observation.rdo=(2u<<28)|(300u<<10)|300u; /* Wrong maximum for this advertisement. */
  tick(&sequence,&sample);
  CHECK(sequence.state == PD_SEQUENCE_FAULT && !sequence.outputs.host_allow && !sequence.outputs.release_bridge);
}
int main(void) {
  for (uint8_t bits=0; bits<3; ++bits) {
    check_ready_five_amp_source(bits);
    check_slow_discharge(bits);
    struct pd_sequence sequence = {0};
    struct pd_sequence_sample sample = {.selector_bits=bits};
    make_ready(&sequence, &sample);
    for (unsigned fault=0; fault<12; ++fault) {
      struct pd_sequence failed = sequence;
      struct pd_sequence_sample bad = sample;
      bad.adc_sample_ms = bad.now_ms;
      switch (fault) {
      case 0: bad.observation.attached=false; break;
      case 1: bad.observation.communication_ok=false; break;
      case 2: bad.hard_reset=true; break;
      case 3: bad.observation.source_generation++; break;
      case 4: bad.selector_bits=3; break;
      case 5: bad.adc_valid=false; break;
      case 6: bad.adc_sample_ms=bad.now_ms-6u; break;
      case 7: bad.adc_sample_ms=bad.now_ms+1u; break;
      case 8: bad.now_ms+=6u; bad.adc_sample_ms=bad.now_ms; break;
      case 9: bad.observation.motor_fault=true; break;
      case 10: bad.observation.motor_rail=exact(0); break;
      case 11: bad.observation.rdo=(1u<<28)|(300u<<10)|300u; break;
      }
      pd_sequence_step(&failed, &bad);
      CHECK(failed.state == PD_SEQUENCE_FAULT);
      CHECK(!failed.outputs.host_allow && !failed.outputs.release_bridge);
      CHECK(failed.outputs.drive_9v == sequence.outputs.drive_9v);
      CHECK(failed.outputs.drive_12v == sequence.outputs.drive_12v);
      tick(&failed, &sample);
      CHECK(failed.state == PD_SEQUENCE_FAULT); /* No automatic fault restart. */
    }
    const struct pd_sequence_request request = {.plan=sequence.plan, .now_ms=sample.now_ms};
    CHECK(!pd_sequence_begin(&sequence, &request)); /* Abort before replanning. */
    pd_sequence_abort(&sequence);
    sample.completed_request_id = sequence.outputs.request_id;
    sample.ps_rdy_request_id = sequence.outputs.request_id;
    const uint32_t old_request_id = sequence.outputs.request_id;
    CHECK(pd_sequence_begin(&sequence, &request));
    CHECK(sequence.outputs.request_id == old_request_id+1);
    const bool retained_9v = sequence.outputs.drive_9v;
    const bool retained_12v = sequence.outputs.drive_12v;
    for (unsigned i=0; i<3001; ++i) tick(&sequence, &sample);
    CHECK(sequence.state == PD_SEQUENCE_FAULT); /* Charged rail must decay first. */
    CHECK(sequence.outputs.drive_9v == retained_9v && sequence.outputs.drive_12v == retained_12v);
  }
  /* An apparently valid but unsafe 12 V / 15 V plan cannot bypass power policy. */
  struct pd_sequence sequence = {0};
  struct pd_sequence_request request = {.plan={
    .valid=true, .motor_voltage_v=12, .voltage_mv=15000, .current_ma=3000,
    .source_generation=1, .source_object_position=2, .sink_pdo=(300u<<10)|300u,
    .source_pdo=(300u<<10)|300u
  }};
  CHECK(!pd_sequence_begin(&sequence, &request));
  request.plan.motor_voltage_v=9;
  CHECK(!pd_sequence_begin(&sequence, &request)); /* 9 V/15 V also lacks reserve. */
  request.plan.voltage_mv=20000;
  request.plan.sink_pdo=(400u<<10)|300u;
  request.plan.source_pdo=(400u<<10)|300u;
  CHECK(pd_sequence_begin(&sequence, &request));
  struct pd_sequence_sample sample = {.adc_valid=true, .selector_bits=1,
    .observation={.attached=true, .communication_ok=true, .source_generation=1,.vbus=exact(5000),.motor_rail=exact(0)}};
  for (unsigned i=0; i<2010; ++i) tick(&sequence, &sample);
  CHECK(sequence.state == PD_SEQUENCE_FAULT && !sequence.outputs.host_allow);
  sample=(struct pd_sequence_sample){.selector_bits=2};
  make_ready(&sequence, &sample);
  struct pd_sequence starting=sequence;
  starting.state=PD_SEQUENCE_RAIL;
  starting.outputs.release_bridge=false;
  starting.entered_ms=sample.now_ms;
  starting.rail_stable=false;
  sample.observation.motor_rail=exact(0);
  sample.observation.motor_fault=true;
  for (unsigned i=0; i<201; ++i) tick(&starting, &sample);
  CHECK(starting.state==PD_SEQUENCE_FAULT && !starting.outputs.host_allow);
  sample=(struct pd_sequence_sample){.selector_bits=2};
  make_ready(&sequence, &sample);
  starting=sequence;
  starting.state=PD_SEQUENCE_WAKE;
  starting.entered_ms=sample.now_ms-2u;
  sample.observation.motor_fault=true;
  tick(&starting, &sample);
  CHECK(starting.state==PD_SEQUENCE_FAULT && !starting.outputs.release_bridge);
  sample=(struct pd_sequence_sample){.selector_bits=1};
  make_ready(&sequence, &sample);
  sample.observation.motor_rail=exact(9541);
  tick(&sequence, &sample);
  CHECK(sequence.state==PD_SEQUENCE_FAULT && !sequence.outputs.host_allow);
  sequence.outputs.request_id=UINT32_MAX;
  request.plan.motor_voltage_v=9;
  CHECK(!pd_sequence_begin(&sequence, &request)); /* Never reuse request token zero. */
  printf("PD sequence host tests passed: %u assertions; embedded and physical tests pending\n", checks);
  return 0;
}