/** * @file pwm.h * @brief Compact single-header API for the VP44 PWM solenoid controller. * * Public API is just two functions: * pwm_init() — one-time setup; caches cal/flash/getters into rt * pwm_service() — per-cycle update; pulls external inputs via getters, * runs the 6-stage control pipeline, writes rt outputs * * External inputs (sensors, CAN, HW) arrive exclusively through the * pwm_input_getters_t vtable. Each getter returns a value in native PWM * units — put any unit/scale conversion inside your getter body. * * Every arithmetic choice (MUL vs MULU, SHRA vs SHR, JGE vs JC) mirrors the * MCS-96 assembly; see src/ for per-stage commentary + disassembly address * cross-references. */ #ifndef PWM_H #define PWM_H #include #include #include /* ── MCS-96 arithmetic primitives ───────────────────────────────────── */ #define MUL_S16(a, b) ((int32_t)(int16_t)(a) * (int32_t)(int16_t)(b)) #define MULU_U16(a, b) ((uint32_t)(uint16_t)(a) * (uint32_t)(uint16_t)(b)) #define CLAMP(v, lo, hi) ((v) < (lo) ? (lo) : (v) > (hi) ? (hi) : (v)) #define INTEG_HI(s) ((int16_t)(((s) >> 16) & 0xFFFF)) #define INTEG_LO(s) ((uint16_t)((s) & 0xFFFF)) /** Portable signed right shift — guarantees sign extension (SHRA/SHRAL). */ static inline int16_t shra16(int16_t v, unsigned n) { if (!n) return v; uint16_t u = (uint16_t)v, sign = (u >> 15) & 1u; uint16_t m = sign ? (uint16_t)(((uint16_t)~0u) << (16u - n)) : 0u; return (int16_t)((u >> n) | m); } static inline int32_t shra32(int32_t v, unsigned n) { if (!n) return v; uint32_t u = (uint32_t)v, sign = (u >> 31) & 1u; uint32_t m = sign ? (~(uint32_t)0u << (32u - n)) : 0u; return (int32_t)((u >> n) | m); } /* ── Mode flag bit definitions (address 0x028c) ─────────────────────── */ #define MODE_FIRST_CALL 0x01 /* open→closed transition */ #define MODE_OUTER_REGION 0x02 /* error outside center region */ #define MODE_NEG_SAT 0x04 /* lower saturation */ #define MODE_POS_SAT 0x08 /* upper saturation */ #define MODE_LARGE_POS 0x10 /* large positive error */ #define MODE_LARGE_NEG 0x20 /* large negative error */ #define MODE_PREV_MASK 0xC0 /* shadow of prev cycle bits 4-5 */ #define INTERP_MAX_ENTRIES 16 /* ══════════════════════════════════════════════════════════════════════ * 1D INTERPOLATION SLOT * Decoded by eval_submap once per input, then consumed by combine_submaps * (bilinear, takes two slots) or refine_submap (1D, takes one slot). * Four int16s; no cycle-to-cycle state. * ══════════════════════════════════════════════════════════════════════ */ typedef struct pwm_interp_slot { int16_t row_stride; /* count * 2 — Y-table row byte-stride (A-axis width) */ int16_t x_interval; /* x[k-1] - x[k] — denominator (dx between breaks) */ int16_t x_offset; /* input - x[k] — numerator (distance from lower break) */ int16_t y_byte_off; /* k * 2 — Y-table byte-offset at break k */ } pwm_interp_slot_t; /* ══════════════════════════════════════════════════════════════════════ * EXTERNAL INPUTS * Populated each cycle by pwm_service via the getter vtable. All values * must be in native PWM units — do any unit/scale conversion inside your * getter. Address comments are the MCS-96 RAM location of the original * variable in the ROM (for cross-reference with disassembly only). * ══════════════════════════════════════════════════════════════════════ */ typedef struct pwm_inputs { int16_t ckp_in; /* 0x02f8 — CKP-derived sensed position */ uint16_t rpm; /* 0x0040 — engine RPM */ int16_t angle_dec_cmd; /* 0x0042 — CAN: angle-decrease command (sp2_B submap input) */ int16_t inj_qty_demand; /* 0x0044 — CAN: injection quantity demand (sp0_B + PI large-neg threshold) */ int16_t b_fb_kw; /* 0x02b4 — CAN: B_FB_KW plunger feedback demand (setpoint baseline) */ int16_t state_130; /* 0x0130 — CAN: open/closed-loop discriminant (< 0 forces open-loop) */ uint16_t supply_voltage; /* 0x0142 — battery / supply voltage (shape_eval submap input) */ int16_t temperature; /* 0x0146 — temperature (sp1_B submap input) */ /* NOTE: pwm_B's submap input_var=0x0046 in the ROM. That address is RW46, * the register into which finalize_angle_request writes active_request * at exit (disasm 0x73f4). It is NOT an external input; our port binds * that descriptor's input_ptr directly to rt->active_request. */ } pwm_inputs_t; /** Getter vtable. Each callback reads one external signal from your system * and returns it in native PWM units. `ctx` is opaque to the PWM library * and is passed back unchanged so your getters can reach their own state. * All callbacks are required (no NULL entries). */ typedef struct pwm_input_getters { int16_t (*ckp_in) (void *ctx); uint16_t (*rpm) (void *ctx); int16_t (*angle_dec_cmd) (void *ctx); int16_t (*inj_qty_demand)(void *ctx); int16_t (*b_fb_kw) (void *ctx); int16_t (*state_130) (void *ctx); uint16_t (*supply_voltage)(void *ctx); int16_t (*temperature) (void *ctx); void *ctx; /* user data; forwarded to every getter */ } pwm_input_getters_t; /* Forward decls for the vtables */ typedef struct pwm_calibration pwm_calibration_t; typedef struct pwm_flash pwm_flash_t; typedef struct pwm_submap_descr pwm_submap_descr_t; /* ══════════════════════════════════════════════════════════════════════ * RUNTIME STATE * Holds everything that persists cycle-to-cycle plus per-cycle working * values. External inputs live in rt->inputs (populated each cycle). * ══════════════════════════════════════════════════════════════════════ */ typedef struct pwm_runtime { /* External inputs — refreshed each cycle by pwm_service from getters */ pwm_inputs_t inputs; /* Async / mixed-direction flags. * reset_flag — set by user/ISR on reset edge; cleared by supervisor. * system_flags_110 — bit 5 (0x20): external "force open-loop" request; * bit 0 (0x01): internal latch set by fast_recovery, * cleared by PI when error_persist_counter hits zero. */ uint8_t reset_flag; /* 0x002e */ uint8_t system_flags_110; /* 0x0110 */ /* ── Setpoint pipeline ───────────────────────────────────────────── */ int16_t compensation_angle; /* 0x02b6 — sum of 3 submap-pair outputs */ int16_t pre_offset_target; /* 0x012a — (b_fb_kw + compensation_angle) >> 1 */ int16_t setpoint_offset; /* 0x0150 — user-supplied setpoint bias */ int16_t target; /* RW5E — final setpoint (clamped) */ pwm_interp_slot_t setpoint_slot_x; /* 0x02b8 — per-call eval → combine */ pwm_interp_slot_t setpoint_slot_y; /* 0x02c0 */ /* ── Supervisor ──────────────────────────────────────────────────── */ int16_t cl_enable_counter; /* 0x0340 */ int16_t cl_error_delta; /* 0x0342 */ int16_t max_cl_error; /* 0x02f2 */ int16_t supervisor_state_17e; /* 0x017e — CL-correction accumulator */ /* ── PI controller ───────────────────────────────────────────────── */ int16_t angle_error; /* 0x0278 */ int16_t angle_error_shaped; /* 0x0276 */ uint8_t mode_flags; /* 0x028c */ int32_t integrator_state; /* 0x0288/028a */ int16_t integrator_gain; /* 0x0286 */ int16_t active_request; /* 0x0274 — final angle request */ int16_t estimated_angle; /* 0x02cc */ int16_t angle_offset; /* 0x017c */ int16_t cl_correction_raw; /* 0x0176 */ int16_t recovery_counter; /* 0x0118 */ int16_t recovery_count_threshold;/* 0x027a */ int16_t error_persist_counter; /* 0x027c */ int16_t first_call_gain; /* 0x02ec */ int16_t pos_error_normalizer; /* 0x02ee */ int16_t neg_error_normalizer; /* 0x02f0 */ int16_t min_rpm_openloop; /* 0x015c */ /* PI shaping (populated from CAL by pwm_init) */ int16_t upper_breakpoint, lower_breakpoint; int16_t center_slope, upper_outer_slope, lower_outer_slope; int16_t upper_integrator_gain, center_integrator_gain, lower_integrator_gain; /* ── PWM output ──────────────────────────────────────────────────── */ uint16_t pwm_duty; /* 0x02d2 */ uint16_t pwm_period; /* 0x0330 */ uint16_t pwm_on_time, pwm_off_time; /* 0x02ce / 0x02d0 — HW compare regs */ uint8_t pwm_status_flag; /* 0x00d1 */ int16_t shape_scale; /* 0x0338 */ int16_t shape_intermediate; /* 0x0332 */ int16_t shape_output; /* 0x033a */ uint16_t pwm_min, pwm_max; /* 0x0158 / 0x015a */ pwm_interp_slot_t pwm_slot_x; /* 0x0290 — pwm_A eval → combine */ pwm_interp_slot_t pwm_slot_y; /* 0x0298 — pwm_B eval → combine */ /* ── Bindings (set once by pwm_init, consumed by pwm_service) ───── */ const pwm_calibration_t *bound_cal; const pwm_flash_t *bound_flash; const pwm_input_getters_t *bound_getters; } pwm_runtime_t; /* ── Calibration (TABLE[RWA4]+offset) ───────────────────────────────── */ struct pwm_calibration { int16_t target_minimum; /* CAL+0x11E */ /* Y-table pointers for each submap pair. Each field originally held a * 16-bit ROM address (CAL+0x184/186/188). [0]=RPM×InjQty, [1]=RPM×Temp, * [2]=RPM×AngleDec — see src/submap_inputvars.txt */ const int16_t *y_pair[3]; /* CAL+0x184/186/188 */ /* PI shaping (CAL+0x00FE…0x0116) */ int16_t pi_upper_breakpoint, pi_lower_breakpoint; int16_t pi_center_slope, pi_upper_outer_slope, pi_lower_outer_slope; int16_t pi_upper_integrator_gain, pi_center_integrator_gain, pi_lower_integrator_gain; /* compute_closed_loop_correction tuning */ int16_t closed_loop_gain_const; /* CAL+0x0156 */ /* fast_recovery tuning */ int16_t error_persist_init_count; /* CAL+0x0108 */ int16_t recovery_rpm_threshold; /* CAL+0x011A */ /* Setpoint offset latched once at init by FUN_8643 (0x867d): * RAM[0x0150] = CAL[0x52] - (CAL[0x54] + RAM[0x0430]) * RAM[0x0430] is observed as always 0 at runtime, so the effective * constant is CAL[0x52] - CAL[0x54]. */ int16_t setpoint_offset; /* CAL+0x0052 - CAL+0x0054 (RAM 0x0150) */ }; /* ── Flash-resident tables ──────────────────────────────────────────── */ struct pwm_flash { int16_t max_error_x[INTERP_MAX_ENTRIES]; int16_t max_error_y[INTERP_MAX_ENTRIES]; uint16_t max_error_count; int16_t max_error_input_addr; int16_t request_upper_limit; /* FLASH:9734 */ int16_t large_pos_error_thresh; /* FLASH:971a */ int16_t large_neg_error_thresh; /* FLASH:971c */ int16_t inj_qty_demand_thresh; /* FLASH:9722 */ uint16_t rpm_breakpoints[8]; /* FLASH:96fe */ uint16_t rpm_values[8]; /* FLASH:970e */ int16_t shape_scale_src; /* FLASH:9710 */ uint16_t period_max_limit, period_min_limit; /* FLASH:96fa / 96fc */ const int16_t *pwm_y_table; /* FLASH:9768 — 2D Y-table */ const int16_t *shape_y_table; /* FLASH:9772 — 1D Y-table */ }; /* ── Submap descriptor (mirrors calibration-block layout) ───────────── */ struct pwm_submap_descr { uint16_t flags; /* +0 */ const int16_t *input_ptr; /* +2 (bound at runtime) */ uint16_t count; /* +4 */ const int16_t *x; /* +6 */ uint16_t input_addr; /* original address (for input_ptr binding) */ }; /** Bind submap descriptor input_ptr fields to the matching source in rt. * Most descriptors point at fields in rt->inputs (external signals), but * input_addr 0x0046 resolves to rt->active_request — in the ROM that RAM * address is RW46, which finalize_angle_request writes with the PI * output at its exit (disasm 0x73f4), so the pwm_B submap sees the * latest active_request when pwm_output_compute runs. * Recognised input_addr values: 0x0040, 0x0042, 0x0044, 0x0046, 0x0142, * 0x0146. Others are bound to NULL. */ static inline void pwm_bind_submap_inputs(pwm_runtime_t *rt, pwm_submap_descr_t *descrs, uint16_t n) { for (uint16_t i = 0; i < n; i++) { switch (descrs[i].input_addr) { case 0x0040: descrs[i].input_ptr = (const int16_t *)&rt->inputs.rpm; break; case 0x0042: descrs[i].input_ptr = &rt->inputs.angle_dec_cmd; break; case 0x0044: descrs[i].input_ptr = &rt->inputs.inj_qty_demand; break; case 0x0046: descrs[i].input_ptr = &rt->active_request; break; case 0x0142: descrs[i].input_ptr = (const int16_t *)&rt->inputs.supply_voltage; break; case 0x0146: descrs[i].input_ptr = &rt->inputs.temperature; break; default: descrs[i].input_ptr = NULL; break; } } } /* ══════════════════════════════════════════════════════════════════════ * PUBLIC API * ══════════════════════════════════════════════════════════════════════ */ /** One-time setup. Zero-inits internal state, applies PI shaping from * cal, and caches cal/flash/getters into rt for later use by pwm_service. * None of the pointer arguments may be NULL. */ void pwm_init(pwm_runtime_t *rt, const pwm_calibration_t *cal, const pwm_flash_t *flash, const pwm_input_getters_t *getters); /** Per-cycle update. Calls each getter to populate rt->inputs, then * runs the 6-stage control pipeline: * 1. max_cl_error lookup * 2. setpoint computation * 3. supervisor (error + CL-enable + clamping) * 4. closed-loop correction + angle estimate * 5. PI controller * 6. PWM duty + HW register split * Outputs are written to rt (pwm_on_time, pwm_off_time, pwm_period, etc). */ void pwm_service(pwm_runtime_t *rt); /** * Notify the controller of a CKP (crankshaft position) pulse edge. * * ROM equivalent: EPA/CKP ISR at 0x877f, which re-latches ckp_in and sets * reset_flag=1 as its terminal step. In this port, ckp_in is pulled from * the getter on each pwm_service() call, so this hook is responsible only * for the reset_flag edge. The supervisor consumes and clears the flag on * the next pwm_service(), which zeroes supervisor_state_17e so the CL * accumulator does not walk off between pulses. * * Call from your CKP edge ISR (or equivalent polling point). Safe to call * between pwm_service() invocations; do NOT call concurrently with * pwm_service(). Byte store is atomic on all mainstream targets — no * additional locking required. * * Rate on VP44: 4 pulses per engine revolution. At 1200 rpm that is * ~80 Hz / ~12 scheduler ticks at the 1 kHz control cadence. */ static inline void pwm_ckp_isr(pwm_runtime_t *rt) { rt->reset_flag = 1; } /** Utility: 1D descending-X piecewise-linear lookup (helper_from_cal). */ int16_t pwm_interp_lookup(const int16_t *x, const int16_t *y, uint16_t n, int16_t in); /* ── Default & ROM-extracted data ───────────────────────────────────── */ extern const pwm_calibration_t pwm_cal_default; extern const pwm_flash_t pwm_flash_default; extern const pwm_calibration_t pwm_cal_rom; extern const pwm_flash_t pwm_flash_rom; #endif /* PWM_H */