Skip to content

Add Bed zones - #28431

Draft
ellensp wants to merge 12 commits into
MarlinFirmware:bugfix-2.1.xfrom
ellensp:bed-zones
Draft

Add Bed zones#28431
ellensp wants to merge 12 commits into
MarlinFirmware:bugfix-2.1.xfrom
ellensp:bed-zones

Conversation

@ellensp

@ellensp ellensp commented May 11, 2026

Copy link
Copy Markdown
Contributor

Description

This PR adds a BED_ZONES feature that divides a heated print bed into independently controlled heating zones. Each zone has its own heater output pin and thermistor input pin. Zones are selected individually or in groups via a bitmask.

The primary motivation is large-format printers and specialty beds where different physical areas need independent temperature control — e.g. heating only the front half of the bed for a small print, or tuning PID independently per zone.

What is implemented

Configuration (Configuration_adv.h)

  • #define BED_ZONES — opt-in feature flag, guarded by HAS_HEATED_BED
  • BED_ZONES_COUNT — number of zones (1–16, must be an explicit integer literal for preprocessor use)
  • BED_ZONE_HEATER_PINS / BED_ZONE_SENSOR_PINS — per-zone pin arrays; zone 0 always maps to HEATER_BED_PIN / TEMP_BED_PIN
  • BED_ZONE_MASK_COUNT / BED_ZONE_MASKS — optional numbered bitmask presets (no string names; presets referenced by index only)
  • BED_ZONE_PID_VALUES — per-zone default PID coefficients
  • SanityCheck.hstatic_assert validates pin array lengths match BED_ZONES_COUNT; error if BED_ZONES enabled without HAS_HEATED_BED

Temperature Core (temperature.cpp / temperature.h)

  • temp_bed expanded from scalar TempInfo to temp_bed[BED_ZONES_COUNT] array
  • Back-compat accessors: degBed() / wholeDegBed() / degTargetBed() return the hottest active zone value (safety-relevant, matches what the status screen shows)
  • setTargetBed(celsius, mask) — applies target to all zones selected by bitmask
  • isHeatingBed() / isCoolingBed() — zone-mask-aware
  • wait_for_bed() — waits until all active-mask zones reach target within TEMP_BED_HYSTERESIS
  • Bang-bang and PID control loops iterate all zones independently
  • Per-zone ISR PWM output: zone 0 uses WRITE_HEATER_BED / soft_pwm_bed; zones 1+ use extDigitalWrite with soft_pwm_bed_zone[] (required for runtime-variable pins on AVR)
  • Per-zone ADC reads: each zone has its own PrepareTemp_BED_ZONEn / MeasureTemp_BED_ZONEn ISR state pair; analog_to_celsius_bed takes an optional zone parameter
  • BED_ZONE_SENSOR_TYPES — per-zone thermistor type override (defaults to TEMP_SENSOR_BED); full thermistor table dispatch per zone
  • Per-zone thermal runawaytr_state_machine_bed[BED_ZONES_COUNT] gives each zone an independent THERMAL_PROTECTION_BED state machine; MAXTEMP/MINTEMP checked per zone
  • Per-zone heating watchdogwatch_bed_zone[BED_ZONES_COUNT] replaces scalar watch_bed
  • Pin initialisation iterates all zone heater pins via pinMode + extDigitalWrite

G-Code

G-Code Change
M140 / M190 New A<index> (single zone) and K<bitmask> (zone group) parameters; falls back to persistent active mask
M142 (new) Set/report the persistent active zone mask; K<mask>, A<index>, P<preset>
M105 Reports B0:<t>/<target> B1:… B<n>@:<pwm> for all zones
M303 New A<index> parameter to autotune a specific bed zone; autotune finish message and result line use zone-aware format (M304 A<n> P… I… D…)
M304 New A<index> parameter to set/report PID for a specific zone; M503 reports one echo: M304 A<n> … line per zone

EEPROM (settings.cpp)

  • Active zone mask (bed_zone_mask) stored and restored
  • Per-zone PID values (bedZonePIDs[BED_ZONES_COUNT]) stored and restored
  • EEPROM version bumped to V91

LCD Menus

  • menu_temperature.cpp / menu_tune.cpp: single Bed item replaced by per-zone Bed Zone 0Bed Zone N-1 loop when HAS_BED_ZONES; all zone temperatures visible and editable from the standard temperature/tune menus (no separate detail screen needed)
  • menu_advanced.cpp: per-zone P/I/D edit items and per-zone PID autotune items (PID_EDIT_MENU + PID_AUTOTUNE_MENU)
  • language_en.h: added MSG_BED_ZONE_N = _UxGT("Bed Zone ~") (tilde replaced by zone index at render time)
  • Status screen B: field: shows hottest active zone (via updated degBed() / degTargetBed())

Controller Fan

  • CONTROLLER_FAN_BED_HEATING uses zone-mask-aware isHeatingBed()

Probing / Calibration (probe.cpp, G76_M871.cpp)

  • All setTargetBed() calls in probe.cpp and G76_M871.cpp pass no mask and use the zone-aware overload's default mask=0xFFFF — all zones are heated before probing/calibration. Since zone geometry is not stored there is no way to know which zone is under the probe; heating all zones is the only correct behaviour.
  • wholeDegBed(), degTargetBed(), and wait_for_bed_heating() are already zone-aware (return/compare against the hottest active zone). No code changes were required in either file.

LCD Preheat (marlinui.cpp)

  • apply_preheat() passes bed_zone_mask to setTargetBed() so preheat presets (M145 / LCD preheat menu) only heat the currently active zones.

Build / Feature Guards

  • HAS_BED_ZONES macro: true when BED_ZONES enabled and BED_ZONES_COUNT > 1
  • features.ini: HAS_BED_ZONES = build_src_filter=+<src/gcode/temp/M142.cpp>
  • All new code is additive behind #if HAS_BED_ZONES / #else guards — non-zone builds are unaffected

Requirements

  • A board with multiple independently switchable bed heater outputs and thermistor inputs (e.g. a large-format printer controller)
  • HAS_HEATED_BED and PIDTEMPBED recommended (bang-bang also works per-zone)
  • For the simulator: SDL2 + MarlinSimUI from the ellensp/MarlinSimUI test-bed-zones branch (fetched automatically via lib_deps)

Benefits

  • Independent temperature control per bed zone — heat only the area being printed on
  • Per-zone PID tuning and autotune
  • Thermal runaway protection is independent per zone — a failed zone does not mask failures in others
  • Fully back-compatible: non-zone builds (BED_ZONES disabled) compile and behave identically to before; all new paths are behind HAS_BED_ZONES guards

Configurations

Currently tested with:

  • mega2560 (AVR ATmega2560) — clean build, no regressions
  • simulator_linux_debug (Linux native sim) — builds and runs; M105/M140/M190/M142/M303/M304/M503 verified via terminal

test_config_for_sim.zip


Related Issues

  • [FR] Dual zone heated bed support #4112
  • @ellensp

    ellensp commented May 13, 2026

    Copy link
    Copy Markdown
    Contributor Author

    Thoughts? G-codes use? Parameter letters? over all?

    ellensp added 10 commits May 16, 2026 22:16
    - marlinui.cpp: apply_preheat() now passes bed_zone_mask to
      setTargetBed() so LCD preheat presets only heat active zones
    
    - docs/BedZones.md:
      - Fix §3.1 parameter letter I→A ("neither K nor A")
      - Fix §3.5 M304 parameter J→A throughout (code always used A;
        §7.2 was already correct)
      - §4.4 probe.cpp: mark done — setTargetBed default mask=0xFFFF
        heats all zones; wholeDegBed/degTargetBed already zone-aware
      - §4.5 marlinui.cpp: mark done
      - §4.7 G76_M871.cpp: mark done — same no-op rationale as probe.cpp
      - §8 Non-Goals: mark per-zone PID in EEPROM as done (was already
        implemented); remove "Dynamic pin assignment at runtime"
      - Phase 4 status: Partial → Done
    Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

    Projects

    None yet

    Development

    Successfully merging this pull request may close these issues.

    1 participant