#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)