# CVT Controller — halcompile C component Runs in the servo thread (1ms typical). Generates step/dir directly, no external `stepgen` needed. Step pulse is one servo cycle high, one low → max ~500 steps/sec, which through a worm gear is more than enough for variator positioning. Build/install: ``` sudo halcompile --install cvt.comp ``` --- ## 1. `cvt.comp` ```c component cvt "CVT variator controller with integrated step generation"; description """ Closed-loop stepper driving a CVT/variator. Selects a gear from a torque-optimized lookup table based on commanded spindle RPM, ramps an internal position command, and generates step/dir pulses inline. Handles homing sweep (min -> max -> home) on demand. Designed for slow positioning behind a worm reduction, so the servo thread (~1 kHz) handles step generation without needing a separate realtime stepgen. """; // --- inputs from LinuxCNC --- pin in float spindle_cmd_rpm "Commanded spindle RPM (from motion.spindle-speed-out)"; pin in float encoder_pos "Closed-loop encoder position feedback (steps)"; pin in bit home_trigger "Rising edge starts homing sweep"; pin in bit limit_min "Min limit switch (IO line 1)"; pin in bit limit_max "Max limit switch (IO line 2)"; // --- outputs to drive / motion --- pin out bit step "Step pulse to drive"; pin out bit dir "Direction to drive"; pin out bit enable "Drive enable"; pin out bit spindle_at_speed "Asserted when encoder is at commanded position"; pin out bit homed "Latched true after successful homing"; pin out s32 current_gear "Currently selected gear index (debug)"; pin out float cmd_pos_out "Internal commanded step position (debug)"; pin out s32 state_out "State machine state (debug)"; // --- tunables --- param rw float max_velocity = 200.0 "Max ramp rate (steps/sec)"; param rw float tolerance = 5.0 "+/- steps to call it at-speed"; param rw s32 home_sweep_steps = 100000 "Max steps to sweep looking for a limit"; // --- gear table (10 entries). rpm_hi[i] is the upper bound for gear i. --- // edit and `halcompile --install` to retune, or `halcmd setp` at runtime. param rw float rpm_hi_0 = 200; param rw s32 steps_0 = 0; param rw float rpm_hi_1 = 400; param rw s32 steps_1 = 100; param rw float rpm_hi_2 = 600; param rw s32 steps_2 = 200; param rw float rpm_hi_3 = 800; param rw s32 steps_3 = 300; param rw float rpm_hi_4 = 1000; param rw s32 steps_4 = 400; param rw float rpm_hi_5 = 1200; param rw s32 steps_5 = 500; param rw float rpm_hi_6 = 1500; param rw s32 steps_6 = 600; param rw float rpm_hi_7 = 1800; param rw s32 steps_7 = 700; param rw float rpm_hi_8 = 2100; param rw s32 steps_8 = 800; param rw float rpm_hi_9 = 99999; param rw s32 steps_9 = 900; function _ fp; license "GPL"; ;; #include #define S_INIT 0 #define S_TO_MIN 1 #define S_TO_MAX 2 #define S_TO_HOME 3 #define S_READY 4 #define S_FAULT 5 static int state = S_INIT; static double target_pos = 0.0; // where the state machine wants us static double cmd_pos = 0.0; // ramp-limited current command static long pos_fb_steps = 0; // step count we've issued static double home_pos = 0.0; // encoder reading at min limit static double max_pos = 0.0; // encoder reading at max limit static int sweep_count = 0; // safety counter during homing // step pulse generator state: 0=idle, 1=pulse high this cycle, 2=pulse low this cycle, 3=dir-setup wait static int step_phase = 0; static int last_dir = 0; static int prev_home_trig = 0; static int select_gear(double rpm, long *target_steps) { double r = fabs(rpm); if (r < rpm_hi_0) { *target_steps = steps_0; return 0; } if (r < rpm_hi_1) { *target_steps = steps_1; return 1; } if (r < rpm_hi_2) { *target_steps = steps_2; return 2; } if (r < rpm_hi_3) { *target_steps = steps_3; return 3; } if (r < rpm_hi_4) { *target_steps = steps_4; return 4; } if (r < rpm_hi_5) { *target_steps = steps_5; return 5; } if (r < rpm_hi_6) { *target_steps = steps_6; return 6; } if (r < rpm_hi_7) { *target_steps = steps_7; return 7; } if (r < rpm_hi_8) { *target_steps = steps_8; return 8; } *target_steps = steps_9; return 9; } FUNCTION(_) { // detect rising edge on home_trigger int home_edge = (home_trigger && !prev_home_trig); prev_home_trig = home_trigger; // ---------------- state machine: pick target_pos ---------------- switch (state) { case S_INIT: enable = 0; homed = 0; spindle_at_speed = 0; if (home_edge) { enable = 1; sweep_count = 0; target_pos = cmd_pos - (double)home_sweep_steps; // drive toward min state = S_TO_MIN; } break; case S_TO_MIN: if (limit_min) { home_pos = encoder_pos; pos_fb_steps = 0; // call this our zero cmd_pos = 0; target_pos = (double)home_sweep_steps; // sweep to max sweep_count = 0; state = S_TO_MAX; } else if (++sweep_count > home_sweep_steps * 10) { state = S_FAULT; } break; case S_TO_MAX: if (limit_max) { max_pos = encoder_pos; target_pos = 0; // park at home state = S_TO_HOME; } else if (++sweep_count > home_sweep_steps * 10) { state = S_FAULT; } break; case S_TO_HOME: if (fabs(cmd_pos - target_pos) < tolerance && fabs(encoder_pos - home_pos) < tolerance) { homed = 1; state = S_READY; } break; case S_READY: { long target_steps; int gear = select_gear(spindle_cmd_rpm, &target_steps); current_gear = gear; target_pos = (double)target_steps; double err = encoder_pos - (home_pos + cmd_pos); spindle_at_speed = (fabs(err) < tolerance) && (fabs(spindle_cmd_rpm) > 0.1); if (home_edge) { homed = 0; state = S_INIT; } break; } case S_FAULT: enable = 0; spindle_at_speed = 0; homed = 0; if (home_edge) state = S_INIT; break; } // ---------------- ramp cmd_pos toward target_pos ---------------- double dv = max_velocity * ((double)period * 1e-9); // steps allowed this cycle double err = target_pos - cmd_pos; if (fabs(err) <= dv) cmd_pos = target_pos; else if (err > 0) cmd_pos += dv; else cmd_pos -= dv; cmd_pos_out = cmd_pos; state_out = state; // ---------------- step pulse generator ---------------- // strategy: each cycle does one piece of a step: // phase 0: idle, ready to step // phase 1: STEP held high this cycle // phase 2: STEP held low (recovery) // phase 3: dir just changed, wait one cycle for setup time if (!enable) { step = 0; step_phase = 0; return; } if (step_phase == 1) { step = 0; step_phase = 2; return; } if (step_phase == 2) { step_phase = 0; // fall through to potentially start a new step } if (step_phase == 3) { step_phase = 0; // dir setup done return; } long pos_err = (long)cmd_pos - pos_fb_steps; if (pos_err == 0) { step = 0; return; } int want_dir = (pos_err > 0) ? 1 : 0; if (want_dir != last_dir) { dir = want_dir; last_dir = want_dir; step_phase = 3; // one-cycle setup delay step = 0; return; } // issue the step step = 1; step_phase = 1; if (want_dir) pos_fb_steps++; else pos_fb_steps--; } ``` --- ## 2. `cvt_home.ngc` ``` ( CVT homing macro -- sweeps variator min -> max -> home ) o sub M64 P0 ( raise digital-out 0 -> cvt.home-trigger ) G4 P0.2 ( latch ) M65 P0 ( release; component is now sweeping ) M66 P0 L3 Q60 ( wait for cvt.homed via digital-in 0, 60s timeout ) o100 if [#5399 LT 0] (debug, CVT homing timed out) M2 o100 endif o endsub M2 ``` --- ## 3. HAL wiring ``` loadrt cvt addf cvt servo-thread # step/dir straight to your output pins (parport shown -- swap for your hw) net cvt-step cvt.step => parport.0.pin-02-out net cvt-dir cvt.dir => parport.0.pin-03-out net cvt-en cvt.enable => parport.0.pin-04-out # encoder feedback from your closed-loop drive's encoder out net cvt-enc encoder.0.position => cvt.encoder-pos # the two IO lines net cvt-min parport.0.pin-10-in => cvt.limit-min net cvt-max parport.0.pin-11-in => cvt.limit-max # spindle plumbing net spindle-cmd motion.spindle-speed-out => cvt.spindle-cmd-rpm net cvt-at-spd cvt.spindle-at-speed => motion.spindle-at-speed # G-code <-> component handshake net cvt-home-trig motion.digital-out-00 => cvt.home-trigger net cvt-homed cvt.homed => motion.digital-in-00 # tunables setp cvt.max_velocity 200 setp cvt.tolerance 5 # initial gear table (edit live with `halcmd setp cvt.steps_3 350` etc.) # rpm_hi_N = upper bound of gear N's RPM window # steps_N = target step position for gear N ``` --- State `S_FAULT` catches the case where homing sweeps forever without seeing a limit (broken switch, wiring issue, mechanical bind). It latches off until the next home trigger. You can wire `cvt.state_out` to a GUI display if you want to see what it's doing. The gear table lives as 20 individual params instead of an array because halcompile doesn't support array params cleanly. Upside: `halcmd setp cvt.steps_4 425` lets you tune on the fly without rebuilding. Once dialed in, bake the values back into the `.comp` defaults so they survive across boots. `cmd_pos` is in steps from home (which becomes the zero after `S_TO_MIN`). The `pos_fb_steps` counter is what the component has actually emitted, so the step generator naturally follows the ramped command. If the closed-loop drive faults and stalls, encoder feedback will diverge from `cmd_pos + home_pos` and `spindle_at_speed` will stay false — gives LinuxCNC a clean signal to hold the cycle.