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/motor_owner_test.c

/* Reuse the ADC peripheral simulator and its independent regression entry.
 * It implements real W1C/self-clearing MMIO behavior, not an ADC-result stub. */
#define main adc_regression_main
#include "stm32_adc_test.c"
#undef main
#include "motor_owner.h"
struct harness {
  struct model m;
  struct motor_owner owner;
  struct motor_measurement_profile profile;
  struct stusb_nvm_image image;
  uint8_t registers[256];
  unsigned transfers, fail_transfer, commands;
  bool new_alert;
};
static uint32_t bus_now(void *context) { return now_us(&((struct harness *)context)->m); }
static bool owner_read(void *context, const struct stusb_read *r) {
  struct harness *h=context;
  ++h->transfers; h->m.now+=30;
  if(h->fail_transfer==h->transfers) return false;
  CHECK((uint32_t)(r->deadline_us-h->m.now)<UINT32_C(0x80000000));
  if(r->register_address==0x96) {
    h->registers[0x96]&=(uint8_t)~0x10u;
    unsigned sector=h->registers[0x96]&7u; CHECK(sector<5);
    memcpy(h->registers+0x53,h->image.bytes+sector*8,8);
  }
  memcpy(r->bytes,h->registers+r->register_address,r->byte_count);
  if(r->register_address==0x16) {
    h->registers[0x16]=0; h->registers[0x0b]&=(uint8_t)~2u;
  }
  return true;
}
static bool owner_write(void *context, const struct stusb_write *w) {
  struct harness *h=context;
  CHECK(!h->m.gpio.host_allow && !h->m.gpio.release_bridge);
  ++h->transfers; h->m.now+=30;
  if(h->fail_transfer==h->transfers) return false;
  CHECK((uint32_t)(w->deadline_us-h->m.now)<UINT32_C(0x80000000));
  memcpy(h->registers+w->register_address,w->bytes,w->byte_count);
  if(w->register_address==0x1a) ++h->commands;
  return true;
}
static bool critical_active;
static unsigned pending_calls, inject_pending_at;
static bool pending(void *context) {
  CHECK(critical_active);
  ++pending_calls;
  return ((struct harness *)context)->new_alert ||
    (inject_pending_at && pending_calls>=inject_pending_at);
}
static uint32_t enter_critical(void *context) {
  (void)context; CHECK(!critical_active); critical_active=true; return 0x1234;
}
static void leave_critical(void *context, uint32_t previous_mask) {
  (void)context; CHECK(critical_active && previous_mask==0x1234); critical_active=false;
}
static void owner_setup(struct harness *h) {
  critical_active=false; pending_calls=inject_pending_at=0;
  memset(h,0,sizeof *h); setup(&h->m); CHECK(motor_adc_initialize(&h->m.port));
  h->m.codes[0]=2257; h->m.codes[1]=1354;
  h->m.a.IDR=(2u<<2)|(1u<<12); h->m.c.IDR=1u<<15;
  /* Synthetic qualification verifies integration only. It is never exported
   * as a manufacturing profile or proof of the receiver's SOP behavior. */
  h->profile=(struct motor_measurement_profile){.approved=true,.qualification_id=1,
    .factory_vref=1650,.adc_error_half_counts=14,.factory_error_half_counts=14,
    .noise_half_counts=4,.upper_divider_ppm=6000,.lower_divider_ppm=8100,
    .reference_drift_ppm=9000,.leakage_na=70,.filter_tau_us=1100};
  for(unsigned i=0;i<40;++i) h->image.bytes[i]=(uint8_t)(i+27);
  h->registers[0x0e]=1; h->registers[0x29]=0x18;
  uint32_t rdo=(2u<<28)|(300u<<10)|300u;
  for(unsigned i=0;i<4;++i) h->registers[0x91+i]=(uint8_t)(rdo>>(8*i));
  h->owner=(struct motor_owner){.gpio=&h->m.gpio,.adc=&h->m.port,
    .bus={.context=h,.now_us=bus_now,.read=owner_read,.write=owner_write},
    .config={.nvm_image=&h->image,.measurement_profile=&h->profile,
      .nvm_qualification_id=1,.receive_path_qualification_id=1},
    .pending_interrupt=pending,.interrupt_context=h,
    .enter_critical=enter_critical,.leave_critical=leave_critical};
}
static void check_owner_off(struct harness *h) {
  CHECK(!h->m.gpio.host_allow && !h->m.gpio.release_bridge);
  CHECK((h->m.a.ODR&0xc0u)==0x80u);
}
static void step(struct harness *h) {
  const struct motor_owner_event event={.environment_qualified=true};
  motor_owner_step(&h->owner,&event);
}
static void send_message(struct harness *h,unsigned type,bool capabilities) {
  uint16_t header=0x140u|type|(capabilities?2u<<12:0);
  h->registers[0x30]=capabilities?8:0;
  h->registers[0x31]=(uint8_t)header; h->registers[0x32]=(uint8_t)(header>>8);
  uint32_t pdos[2]={(100u<<10)|300u,(400u<<10)|300u};
  for(unsigned i=0;i<8;++i) h->registers[0x33+i]=(uint8_t)(pdos[i/4]>>(8*(i%4)));
  h->registers[0x16]=4; h->registers[0x0b]=2;
  const struct motor_owner_event event={.alert_pending=true,.alert_us=h->m.now,
    .environment_qualified=true};
  motor_owner_step(&h->owner,&event);
}
static void acquire_and_request(struct harness *h) {
  CHECK(motor_owner_initialize(&h->owner)); CHECK(h->owner.nvm.image_matches);
  step(h); CHECK(h->owner.acquiring && h->commands==1);
  send_message(h,3,false); CHECK(h->owner.acquiring); /* SoftReset Accept. */
  send_message(h,1,true); CHECK(!h->owner.acquiring);
  CHECK(h->owner.sequence.state==PD_SEQUENCE_DECAY);
  CHECK(!h->m.gpio.feedback_known); check_owner_off(h);
  /* A high/back-driven VM is safe waiting, not a feedback-change attempt. */
  step(h); CHECK(h->owner.fault==MOTOR_OWNER_OK); CHECK(!h->m.gpio.feedback_known);
  h->m.codes[1]=56; step(h); CHECK(h->owner.sequence.state==PD_SEQUENCE_FEEDBACK);
  CHECK(h->m.gpio.feedback_known && h->m.gpio.drive_12v);
  for(unsigned i=0;i<20 && !h->owner.request.completed_request_id;++i) {
    h->m.now+=500; step(h); CHECK(h->owner.fault==MOTOR_OWNER_OK); check_owner_off(h);
  }
  CHECK(h->owner.request.completed_request_id==1 && h->commands==2);
  step(h); CHECK(h->owner.sequence.state==PD_SEQUENCE_CONTRACT);
}
static void qualify_contract(struct harness *h) {
  send_message(h,3,false); CHECK(!h->owner.requested_accept);
  send_message(h,1,true); CHECK(h->owner.requested_capabilities);
  send_message(h,3,false); CHECK(h->owner.accept_request_id==1);
  send_message(h,6,false);
}
static void ready(struct harness *h) {
  acquire_and_request(h); qualify_contract(h);
  CHECK(h->owner.fault==MOTOR_OWNER_OK && h->owner.sequence.state==PD_SEQUENCE_RAIL);
  CHECK(h->m.gpio.host_allow && !h->m.gpio.release_bridge);
  h->m.codes[1]=1354;
  for(unsigned i=0;i<40 && h->owner.sequence.state!=PD_SEQUENCE_READY;++i) {
    h->m.now+=500; step(h); CHECK(h->owner.fault==MOTOR_OWNER_OK);
  }
  CHECK(h->owner.sequence.state==PD_SEQUENCE_READY && h->m.gpio.release_bridge);
}
int main(void) {
  struct harness h;
  for(unsigned i=0;i<6;++i) {
    owner_setup(&h);
    switch(i) {
      case 0: h.owner.config.nvm_image=NULL; break;
      case 1: h.owner.config.nvm_qualification_id=0; break;
      case 2: h.owner.config.receive_path_qualification_id=0; break;
      case 3: h.owner.config.measurement_profile=NULL; break;
      case 4: h.profile.approved=false; break;
      default: h.profile.qualification_id=0; break;
    }
    CHECK(!motor_owner_initialize(&h.owner)); CHECK(h.owner.fault==MOTOR_OWNER_CONFIGURATION_MISSING);
    unsigned transfers=h.transfers; step(&h); CHECK(h.transfers==transfers);
    CHECK(!h.owner.last_measurement.valid); check_owner_off(&h);
  }
  owner_setup(&h); ready(&h);
  /* Detach, hardware alert, protocol reset, missing original edge, I2C,
   * ADC, clock gap, brownout, qualification loss and new pending IRQ. */
  for(unsigned i=0;i<10;++i) {
    owner_setup(&h); ready(&h);
    struct motor_owner_event e={.environment_qualified=true};
    switch(i) {
      case 0: h.registers[0x0e]=0; break;
      case 1: h.registers[0x0b]=0x20; break;
      case 2: h.registers[0x16]=0x80; e.alert_pending=true; e.alert_us=h.m.now; break;
      case 3: h.registers[0x0b]=2; break;
      case 4: h.fail_transfer=h.transfers+1; break;
      case 5: h.m.fault=OVERRUN; break;
      case 6: h.m.now+=5001; break;
      case 7: e.brownout=true; break;
      case 8: e.environment_qualified=false; break;
      default: h.new_alert=true; break;
    }
    motor_owner_step(&h.owner,&e); CHECK(h.owner.fault!=MOTOR_OWNER_OK); check_owner_off(&h);
    CHECK(h.m.gpio.drive_12v); /* Charged-rail feedback retained. */
  }
  /* Event arriving between precheck and postcheck must revoke before unmask. */
  owner_setup(&h); ready(&h); inject_pending_at=pending_calls+2;
  step(&h); CHECK(h.owner.fault==MOTOR_OWNER_PD_EVENT); check_owner_off(&h);
  CHECK(!critical_active);
  owner_setup(&h); acquire_and_request(&h); h.m.a.IDR|=1u<<8;
  qualify_contract(&h); CHECK(h.owner.fault!=MOTOR_OWNER_OK); check_owner_off(&h);
  owner_setup(&h); ready(&h); h.m.a.IDR=(1u<<2)|(1u<<12);
  step(&h); check_owner_off(&h); CHECK(h.m.gpio.drive_12v && h.owner.acquiring);
  send_message(&h,1,true); CHECK(h.owner.sequence.state==PD_SEQUENCE_DECAY);
  CHECK(h.m.gpio.drive_12v); /* 9V feedback cannot change at charged12V. */
  owner_setup(&h); acquire_and_request(&h); send_message(&h,6,false);
  CHECK(h.owner.fault==MOTOR_OWNER_PD_EVENT); check_owner_off(&h);
  owner_setup(&h); h.m.now=UINT32_MAX-12000u; ready(&h);
  CHECK(h.owner.uptime_ms<100 && h.owner.fault==MOTOR_OWNER_OK);
  puts("Serialized motor-owner integration/fault tests passed");
  printf("%u assertions\n",assertions);
  return 0;
}