#cnc #vfd #h100 #modbus #rs485 #grblhal #reprapfirmware #arborctl #voroncascade #handoff # Overview This note is a **self-contained handoff** capturing everything needed to drive a G-Penny H100 VFD over Modbus RTU from either grblHAL or RepRapFirmware (RRF) + MillenniumOS + ArborCTL. It is intended to be read by Claude Code (or another agent) without needing the rest of the Voron Cascade vault — every fact required to write working firmware config or macros should be in this single file. Companion note: [[RepRapFirmware and MillenniumOS on BTT Scylla]] holds the broader Cascade build plan. This note is the deep-dive for the spindle/VFD slice of that plan. The deliverable Claude Code is most likely to be asked to produce from this note: either a grblHAL `
-settings dump that drives the H100 via MODVFD, *or* an RRF `daemon.g` macro (and/or an ArborCTL profile fork) that talks H100 Modbus directly. # The H100 hardware ## What "H100" actually is The G-Penny H100 is a rebadge of a generic Chinese vector-control inverter family. The same physical hardware ships under multiple labels: - G-Penny H100 (the Cascade BOM choice) - StepperOnline V70 - Huanyang H100 (note: this is **not** the same as the Huanyang HY series — different register map, different parameter naming) - Various other "H100"-marked boxes All share the same parameter map (`F001`, `F002`, `F163`, …) and the same Modbus register layout. When researching, treat "H100" and "V70" as equivalent search terms; **HY-series documentation does not apply.** ## Electrical / mechanical context (Cascade BOM) - Spindle: G-Penny 1.5 kW 24 000 RPM ER16 - VFD input: 220 V single-phase (model varies; check the unit) - VFD output: 3-phase to spindle - Spindle minimum useful RPM: ~6 000 (below this, torque collapses) - Spindle maximum RPM: 24 000 ## H100 manual Official PDF mirror: [H100 Operation Manual](https://images.thdstatic.com/catalog/pdfImages/b0/b008b752-3e4a-4c5c-aa62-3f425c50eff9.pdf). Modbus section starts at page ~79; F-parameter table starts around page 20. # Modbus RTU configuration ## Serial parameters | Parameter | Value | |---|---| | Protocol | Modbus RTU | | Baud rate | 38400 (max the H100 supports) | | Data bits | 8 | | Parity | Even (`E`) — LinuxCNC community default | | Stop bits | 1 | | Framing string | `8E1` | | Slave address (default) | 1 | | Wiring | Twisted pair, A→`RS+`, B→`RS−`, shield grounded one end | ==`8N1` is also valid on the H100. Pick one and use it on both ends. The LinuxCNC community has converged on `8E1`; ArborCTL examples for other VFDs use varying framings. Whatever is chosen, `F165` on the VFD must match.== ## VFD-side parameters (set on the H100 keypad before any Modbus talks) | Param | Value | Meaning | |---|---|---| | `F001` | `2` | Run command source = external (Modbus) | | `F002` | `2` | Frequency command source = external (Modbus) | | `F163` | `1` | Modbus slave address = 1 | | `F164` | `3` | Baud rate = 38400 | | `F165` | `3` | Data format = 8E1 (see note above) | | `F169` | `0` | Response delay = 0 ms | | `F011` | `100` | Lower frequency limit, Hz × 1 — keep at 6000 RPM minimum spindle equivalent (100 Hz on a 2-pole motor at 60 Hz base = 6000 RPM; G-Penny is high-frequency so verify against the spindle nameplate) | Power-cycle the VFD after changing these. The H100 will not respond to Modbus until `F001=2`, `F002=2`, and the comms params (`F163`–`F165`) are committed. ## Register map (verified against community-documented H100/V70) All addresses are **decimal** (use these directly in `M260.1` / `M261.1` / grblHAL `$462–$464`). Hex shown for cross-reference to the manual. | Register | Decimal | Hex | Direction | Function | Encoding | |---|---|---|---|---|---| | Run/Stop command | 512 | `0x0200` | Write | 1 = run forward, 2 = run reverse, 5 = stop | uint16 | | Set frequency | 513 | `0x0201` | Write | Target frequency in 0.01 Hz units (e.g. 24000 RPM at 2-pole base = 400 Hz → write 40000) | uint16, 0.01 Hz | | Output frequency | 544 | `0x0220` | Read | Actual running frequency in 0.01 Hz units | uint16, 0.01 Hz | | Output current | 545 | `0x0221` | Read | Motor current | uint16, 0.1 A | | Fault code | 546 | `0x0222` | Read | 0 = no fault, non-zero = fault index per H100 manual fault table | uint16 | ==There are small variations across H100 rebadges. Before relying on these in a production macro, dump the register space with `mbpoll` against the actual VFD on the bench and compare. See the LinuxCNC reference below for a working scan script.== ## RPM ↔ frequency math The G-Penny 1.5 kW 24 000 RPM spindle is a 2-pole motor. The standard relationship: ``` RPM = Hz × 60 / pole_pairs (pole_pairs = 1 for a 2-pole motor) RPM = Hz × 60 Hz = RPM / 60 ``` H100 register units are 0.01 Hz, so: ``` register_value = RPM × 100 / 60 = RPM × 5/3 ``` Example: 18 000 RPM → 300 Hz → register value `30000`. # Path 1 — grblHAL with the MODVFD generic driver This is the shorter path. grblHAL does not ship a dedicated H100 driver, but the **MODVFD** generic Modbus driver (modeled on LinuxCNC's `vfdmod`) covers any well-behaved Modbus RTU VFD via runtime `
-settings. No firmware code changes are required if the build already includes MODVFD. ## Build prerequisites In `my_machine.h` for the grblHAL build: ```c #define VFD_SPINDLE -1 // -1 = compile in all spindle drivers #define MODBUS_ENABLE 1 // ensure Modbus support is on ``` Setting `VFD_SPINDLE` to a specific driver number works too — see [grblHAL/Plugins_spindle](https://github.com/grblHAL/Plugins_spindle) for the enum. `-1` is simplest because the active driver is then chosen at runtime via `$395`. ## Runtime settings (set via console or ioSender) ```text $395 = <MODVFD spindle id> ; bind spindle 0 to MODVFD; query with `$spindles` to find the id $360 = 1 ; Modbus slave address — matches H100 F163 $461 = 60 ; RPM-to-Hz factor for "at speed" computation $462 = 512 ; Run/stop register (H100 0x0200) $463 = 513 ; Set frequency register (H100 0x0201) $464 = 544 ; Get frequency register (H100 0x0220) $465 = 1 ; Command word for CW run $466 = 2 ; Command word for CCW run $467 = 5 ; Command word for stop $468 = 100 ; RPM → register multiplier (0.01 Hz units) $469 = 60 ; RPM → register divider (RPM/60 = Hz, then ×100 in multiplier) $470 = 60 ; register → RPM multiplier $471 = 100 ; register → RPM divider $30 = 24000 ; max spindle RPM $31 = 6000 ; min spindle RPM (matches G-Penny lower torque limit) ``` After changes to `$395`, a **hard reset** of the controller is required before the new ModBus address settings show up. ioSender / sender-side settings: - Default Spindle → MODVFD (must match `$395`) - Max RPM → 24000 - Min RPM → 6000 - Spindle-on delay → 30 s (gives the VFD time to ramp before checking "at speed") - ModBus baud rate → 38400 ## Wiring - Controller `485A` / `RS485+` → H100 `RS+` - Controller `485B` / `RS485−` → H100 `RS−` - Twisted pair (Cat5 single pair is fine for short runs) - Shield to GND at controller end only - Terminate with 120 Ω across A/B at the VFD end if the run is >2 m or noisy ## Verification 1. With `F001=2`/`F002=2` set and Modbus wired, send `M3 S6000` from the sender. 2. VFD display should show ramp-up; "actual RPM" in ioSender should report back from register 544. 3. `M5` should stop the spindle and the display should ramp down. 4. `M3 S24000` and `M3 S12000` to test the full range. # Path 2 — RRF + MillenniumOS direct Modbus (recommended starting point) ArborCTL has no H100 profile (as of 2025-10 it supports Shihlin SL3, Huanyang HY02D223B, Yalang YL620A, and untested Huanyang GT-2R2G-2). The cleanest first move on RRF is to skip ArborCTL and talk Modbus directly with RRF 3.6+'s `M260.1` / `M261.1` gcodes, driven from `daemon.g`. This is what ArborCTL is doing internally — just hand-rolled for the H100. ## Prerequisites - RRF 3.6.0 or newer running on the BTT Scylla (TeamGloomy STM32H7 port, build `firmware_scylla1_0_h723.bin`) - MillenniumOS installed and configured (see [[RepRapFirmware and MillenniumOS on BTT Scylla]]) - Scylla's onboard RS485 transceiver wired to the H100 `RS+`/`RS−` ## Channel discovery The Scylla exposes its RS485 transceiver on one of the STM32H7's UART channels. The exact RRF "P" channel number is documented in TeamGloomy's [Scylla pin names page](https://teamgloomy.github.io/btt_scylla_v1_h723_pins_3_5.html) — find the entry labeled `RS485` or `485A`/`485B` and note which serial channel it maps to. Substitute that as `<chan>` below. ==Confirm channel number from the live Scylla pin table before flashing.== ## config.g additions ```gcode ; --- RS485 on the Scylla --- ; <chan> is whichever serial channel the Scylla's RS485 transceiver is on M575 P<chan> B38400 S7 ; mode 7 = device/Modbus; baud must match H100 F164 ; --- Spindle declared so MOS/RRF object model sees it --- ; If a real PWM pin exists, use it; otherwise bind to a "dummy" output. M950 R0 C"<pwm_or_dummy_pin>" L0:24000 M453 ; CNC mode ; --- Tool 0 owns spindle 0 --- M563 P0 S"Spindle" R0 ; --- Daemon macro that translates spindle state -> Modbus -> H100 --- M98 P"daemon.g" ``` ## daemon.g (skeleton — adapt and harden) `daemon.g` runs in a loop at ~1 s cadence. The pattern is: read the desired RPM from the RRF object model, compute the H100 register value, write it, then poll readback. ```gcode ; daemon.g — H100 VFD driver ; runs continuously; keep it cheap var chan = 2 ; RS485 channel on Scylla — set to match M575 P<chan> var slave_id = 1 ; matches H100 F163 ; Read desired state from object model var desired_rpm = spindles[0].active var spindle_dir = spindles[0].state ; "stopped" | "forward" | "reverse" ; Compute register value: RPM * 100 / 60 (0.01 Hz units) var freq_reg = floor(var.desired_rpm * 100 / 60) if var.spindle_dir == "forward" M260.1 P{var.chan} A{var.slave_id} F6 R513 B{var.freq_reg} ; set frequency M260.1 P{var.chan} A{var.slave_id} F6 R512 B1 ; run forward elif var.spindle_dir == "reverse" M260.1 P{var.chan} A{var.slave_id} F6 R513 B{var.freq_reg} M260.1 P{var.chan} A{var.slave_id} F6 R512 B2 ; run reverse else M260.1 P{var.chan} A{var.slave_id} F6 R512 B5 ; stop ; Read back actual frequency and fault status (use sparingly to avoid bus saturation) M261.1 P{var.chan} A{var.slave_id} R544 B1 F3 V"hz_actual" M261.1 P{var.chan} A{var.slave_id} R546 B1 F3 V"fault" if var.fault[0] != 0 M291 S2 P{"H100 VFD fault: " ^ var.fault[0]} M0 ; pause job on fault ; (optional) publish actual RPM back into a global for use elsewhere set global.h100_actual_rpm = var.hz_actual[0] / 100 * 60 ``` ==This skeleton is intentionally rough. Production hardening needs: (a) rate-limiting the writes so the bus isn't hammered every daemon tick, (b) detecting state changes (only write when desired changes), (c) timeout/retry on `M260.1`/`M261.1` failures, (d) sanity-clamping `desired_rpm` to `[6000, 24000]`, (e) sending stop before changing direction.== ## Function code reference The `F` parameter in `M260.1` / `M261.1` selects the Modbus function code: | `F` | Function | Use | |---|---|---| | 1 | Read Coils | not used for H100 | | 2 | Read Discrete Inputs | not used for H100 | | 3 | Read Holding Registers | use for register 544 readback (default for `M261.1`) | | 4 | Read Input Registers | alternative; check H100 manual which space the registers live in | | 5 | Write Single Coil | not used for H100 | | 6 | Write Single Register | **use for register 512 / 513 writes** | | 15 | Write Multiple Coils | not used | | 16 | Write Multiple Registers | alternative for back-to-back register writes | H100 documentation indicates holding-register addressing for both reads and writes. Start with `F3` (read holding) and `F6` (write single register). If reads fail, try `F4` (read input registers) — some H100 firmwares place readback in the input register space. # Path 3 — RRF + ArborCTL profile fork Longer-horizon path: add a proper H100 profile to ArborCTL. This integrates with MillenniumOS's daemon framework, gets fault handling, "at speed" detection, and per-VFD configurability "for free," and is contributable back upstream. ## ArborCTL architecture (per the repo) - License: GPL-3.0 - Language: GAP (RRF macros, 99 %) + a sliver of G-code - Repo: [MillenniumMachines/ArborCTL](https://github.com/MillenniumMachines/ArborCTL) - Install pattern: download release zip → upload via DWC → add `M98 P"arborctl.g"` after the spindle declaration in `config.g` → edit `daemon.g` to include `arborctl-daemon.g` Directory structure relevant for this work: - `macro/` — per-VFD profile macros - `sys/` — daemon entry points, including `arborctl-daemon.g` and `daemon.g.example` - `doc/` — protocol docs (read these before forking) ## Fork plan 1. Clone the repo locally; create a feature branch. 2. Identify the cleanest existing profile to copy. Shihlin SL3 is reportedly the most rigorously written. Huanyang HY02D223B may also be a good template because it uses a similar "write run-cmd register / write freq register / read freq register" pattern. 3. Copy the chosen profile to a new `h100/` (or similar) folder under `macro/`. 4. Replace register addresses with H100 values (512 / 513 / 544 / 546). 5. Replace command words: `1` (CW), `2` (CCW), `5` (stop) — these differ from HY and from Shihlin. 6. Replace the RPM→register and register→RPM transforms with the 0.01 Hz scaling shown above. 7. Add an H100 entry to whatever profile-selection mechanism ArborCTL uses (config variable, file naming convention, or `M-code` selector — confirm from the actual code). 8. Update `doc/` with H100 wiring + parameter table. 9. Test on the bench against a real H100 before merging. 10. Open a PR upstream. ==This path requires reading ArborCTL's actual code first — the description above is the structural plan but the framework's exact extension API may have changed. Don't write the profile blind; mirror what an existing supported VFD does and only deviate at register / command-word level.== # Wiring quick reference ``` Scylla side H100 side ----------- --------- RS485_A ──── twisted pair ──── RS+ (or A or D+) RS485_B ──── twisted pair ──── RS- (or B or D-) GND ──── shield (one end) ──── (do not connect to H100) Optional: 120 Ω termination resistor across A/B at the VFD end of the bus ``` The Scylla v1.0 has an onboard RS485 transceiver — no external MAX485 module is required. Confirm `485A`/`485B` labelling against the Scylla silkscreen and the [TeamGloomy Scylla pin names](https://teamgloomy.github.io/btt_scylla_v1_h723_pins_3_5.html). # Testing strategy Bring up in this order, with the spindle physically disconnected from the workpiece and a hand on the e-stop: 1. **VFD keypad control works alone.** Set `F001=0`, `F002=0` (keypad source), confirm spindle starts/stops and ramps via the VFD front panel. This proves the VFD↔spindle electrical path before adding Modbus complexity. 2. **VFD parameters set for external control.** Set `F001=2`, `F002=2`, `F163=1`, `F164=3`, `F165=3`, `F169=0`. Power-cycle. 3. **Bus connectivity.** From the controller, attempt a read of register 544 (`M261.1 P<chan> A1 R544 B1 F3` in RRF, or via grblHAL's spindle status output). Expect 0 (spindle stopped) — any response confirms the bus is alive. No response = wiring or framing wrong. 4. **Single run/stop cycle.** Send a low-RPM run command (e.g. `M3 S6000`). Spindle should ramp to 6000 RPM. Send `M5`. Confirm. 5. **Range sweep.** Step through 6000 / 12000 / 18000 / 24000 with `G4` dwells between. Confirm readback matches command. 6. **Direction.** `M4 S6000` (reverse, if relevant) → `M5` → `M3 S6000` (forward). Some CNC apps disable M4 in CNC mode; check the chosen post-processor. 7. **Fault injection.** Pull a phase wire to trigger an under-current fault, confirm fault register 546 reads non-zero and the daemon handles it. 8. **Long-run stability.** 30-minute spindle-on test at job RPM, watching for bus errors or unexpected stops. # Troubleshooting | Symptom | Likely cause | |---|---| | No Modbus response at all | Framing mismatch (8N1 vs 8E1), wrong baud, A/B swapped, or `F001`/`F002` not set to 2 | | Spindle runs from VFD keypad but ignores Modbus | `F001` or `F002` still at 0 (keypad source). Set to 2 and power-cycle. | | Spindle runs but RPM readback wrong | RPM↔Hz scaling wrong. Verify the spindle is 2-pole (most G-Penny ER16s are). Adjust `$461`/`$470`/`$471` (grblHAL) or the daemon math (RRF). | | Spindle ramps to wrong RPM | Register 513 units wrong — confirm 0.01 Hz scaling, not 0.1 Hz, not raw Hz. | | Intermittent dropouts | Cable not twisted pair, no termination, or shield grounded at both ends. | | Bus saturated / controller stutter | Daemon polling readback too aggressively — rate-limit reads to once per second or once per state change. | `mbpoll` is the canonical bench tool for sanity-checking the VFD over Modbus directly from a laptop with a USB↔RS485 dongle. Example scan from the LinuxCNC reference: ``` mbpoll -m rtu -a 1 -b 38400 -d 8 -P even -s 1 -r 160 -c 4 /dev/ttyUSB0 ``` `-r 160 -c 4` reads registers F163–F166. Vary `-r` to walk the F-parameter space. # Open questions for Claude Code - ==Confirm RRF channel number for the Scylla's onboard RS485 transceiver against [the TeamGloomy Scylla pin names](https://teamgloomy.github.io/btt_scylla_v1_h723_pins_3_5.html). Substitute into the `M575 P<chan>` line and the `var chan` in `daemon.g`.== - ==Verify register addresses 512 / 513 / 544 / 546 against the actual H100 manual that ships with the unit. There are minor variations across rebadges. Bench-test with `mbpoll` before deploying.== - ==Decide 8N1 vs 8E1 framing. Set `F165` on the VFD and the `M575 S7` mode parameter (and the corresponding grblHAL setting) consistently. The community default is 8E1; this note assumes 8E1 throughout.== - ==Decide between Path 2 (raw `M260.1`/`M261.1` daemon, faster to write) and Path 3 (ArborCTL profile fork, more work but better long-term integration). Path 2 first, then evaluate.== - ==For Path 3: read ArborCTL's actual code before writing the H100 profile — particularly how it discovers VFD profiles at install time, whether profiles are auto-loaded or have to be activated by a config setting, and the framework's daemon-tick contract.== - ==Production hardening of the Path 2 daemon: state-change detection (don't write every tick), retry on Modbus timeout, sanity-clamping RPM, ordered stop-before-direction-change, fault-driven `M0` pause.== # Source links - [grblHAL Plugins_spindle README](https://github.com/grblHAL/Plugins_spindle/blob/master/README.md) — MODVFD `$462`–`$471` documentation, `$395` spindle binding - [grblHAL Plugins_spindle repo](https://github.com/grblHAL/Plugins_spindle) - [MillenniumMachines/ArborCTL repo](https://github.com/MillenniumMachines/ArborCTL) — alpha; macro-based RS485 spindle framework for RRF 3.6+ - [Duet3D: Connecting RS485 and Modbus RTU devices](https://docs.duet3d.com/User_manual/Connecting_hardware/RS485_Modbus) — `M575`, `M260.1` (write), `M261.1` (read), function codes - [Duet3D: Configuring RRF for a CNC machine](https://docs.duet3d.com/User_manual/Machine_configuration/Configuration_CNC) — `M453`, `M950 R<n>`, `M563` - [TeamGloomy STM32 RRF home](https://teamgloomy.github.io/) — port of RRF to the Scylla - [TeamGloomy Scylla pin names (RRF 3.5+)](https://teamgloomy.github.io/btt_scylla_v1_h723_pins_3_5.html) — needed to find the RS485 channel number - [LinuxCNC forum: G-Penny H100 register documenting](https://www.forum.linuxcnc.org/49-basic-configuration/57333-g-penny-spindle-h100-register-settings-documenting) — `mbpoll` scan script and parameter map (38400/8E1) - [Onefinity forum: H100 VFD setup walkthrough](https://forum.onefinitycnc.com/t/help-with-h100-vfd-setup-wiring-programming-solved/27022) — F-parameter walkthrough on a different controller; same Modbus map - [H100 manual (mirror)](https://images.thdstatic.com/catalog/pdfImages/b0/b008b752-3e4a-4c5c-aa62-3f425c50eff9.pdf) — register map and F-parameter list - [CNCZone: Modbus address help for H100 Inverter](https://www.cnczone.com/forums/spindles-vfd/473082-modbus-address-help-h100-inverter-new-post.html) - [PrintNC wiki: VFD Control with grblHAL](https://wiki.printnc.info/en/grbl/vfd-control) — practical setup walkthroughs (HY and YL620; adapt the workflow for H100/MODVFD) - [aekhv/vfdmod (LinuxCNC component grblHAL's MODVFD mirrors)](https://github.com/aekhv/vfdmod) — useful reference for understanding the MODVFD register/word model # References - [[RepRapFirmware and MillenniumOS on BTT Scylla]] — broader Cascade build context - [[Voron Cascade BOM]] — running parts list (G-Penny + H100 line item) - [[Electronics]] — Cascade electrical / PSU plan (RS485 cable routing implications)