diff --git a/Marlin/src/core/types.h b/Marlin/src/core/types.h
index af6834c666b3..c403e7439e2b 100644
--- a/Marlin/src/core/types.h
+++ b/Marlin/src/core/types.h
@@ -199,8 +199,6 @@ enum AxisEnum : uint8_t {
   , ALL_AXES_ENUM = 0xFE, NO_AXIS_ENUM = 0xFF
 };
 
-typedef bits_t(NUM_AXIS_ENUMS) axis_bits_t;
-
 //
 // Loop over axes
 //
@@ -789,6 +787,156 @@ struct XYZEval {
   FI bool        operator!=(const XYZEval<T> &rs) const { return !operator==(rs); }
 };
 
+#include <string.h> // for memset
+
+class AxisBits;
+
+class AxisBits {
+public:
+  typedef bits_t(NUM_AXIS_ENUMS) el;
+  union {
+    el bits;
+    struct {
+      union {
+        bool NUM_AXIS_LIST(x:1, y:1, z:1, i:1, j:1, k:1, u:1, v:1, w:1);
+        bool NUM_AXIS_LIST(X:1, Y:1, Z:1, I:1, J:1, K:1, U:1, V:1, W:1);
+        bool NUM_AXIS_LIST(a:1, b:1, c:1, _i:1, _j:1, _k:1, _u:1, _v:1, _w:1);
+        bool NUM_AXIS_LIST(A:1, B:1, C:1, _I:1, _J:1, _K:1, _U:1, _V:1, _W:1);
+      };
+      #if HAS_EXTRUDERS
+        union { bool e:1; bool e0:1; };
+        #define _EN_ITEM(N) bool e##N:1;
+        REPEAT_S(1,EXTRUDERS,_EN_ITEM)
+        #undef _EN_ITEM
+      #endif
+      #if ANY(IS_CORE, MARKFORGED_XY, MARKFORGED_YX)
+        bool hx:1, hy:1, hz:1;
+      #endif
+    };
+  };
+
+  AxisBits() { bits = 0; }
+
+  // Constructor, setter, and operator= for bit mask
+  AxisBits(const el p) { set(p); }
+  void set(const el p) { bits = el(p); }
+  FI AxisBits& operator=(const el p) { set(p); return *this; }
+
+  #define MSET(pE,pX,pY,pZ,pI,pJ,pK,pU,pV,pW) LOGICAL_AXIS_CODE(e=pE, x=pX, y=pY, z=pZ, i=pI, j=pJ, k=pK, u=pU, v=pV, w=pW)
+
+  // Constructor, setter, and operator= for XYZE type
+  AxisBits(const xyze_bool_t &p) { set(p); }
+  void set(const xyze_bool_t &p) {
+    MSET(p.e, p.x, p.y, p.z, p.i, p.j, p.k, p.u, p.v, p.w);
+  }
+  FI AxisBits& operator=(const xyze_bool_t &p) { set(p); return *this; }
+
+  // Constructor, setter, and operator= for bool array
+  AxisBits(const bool (&p)[LOGICAL_AXES]) { set(p); }
+  void set(const bool (&p)[LOGICAL_AXES]) {
+    MSET(p[E_AXIS], p[X_AXIS], p[Y_AXIS], p[Z_AXIS],
+                    p[I_AXIS], p[J_AXIS], p[K_AXIS],
+                    p[U_AXIS], p[V_AXIS], p[W_AXIS]);
+  }
+  FI AxisBits& operator=(const bool (&p)[LOGICAL_AXES]) { set(p); return *this; }
+
+  // Constructor, setter, and operator= for undersized bool arrays
+  #if LOGICAL_AXES > 1
+    AxisBits(const bool (&p)[1]) { set(p); }
+    FI void set(const bool (&p)[1]) {
+      MSET(0, p[X_AXIS], 0, 0, 0, 0, 0, 0, 0, 0);
+    }
+    FI AxisBits& operator=(const bool (&p)[1]) { set(p); return *this; }
+  #endif
+  #if LOGICAL_AXES > 2
+    AxisBits(const bool (&p)[2]) { set(p); }
+    FI void set(const bool (&p)[2]) {
+      MSET(0, p[X_AXIS], p[Y_AXIS], 0, 0, 0, 0, 0, 0, 0);
+    }
+    FI AxisBits& operator=(const bool (&p)[2]) { set(p); return *this; }
+  #endif
+  #if LOGICAL_AXES > 3
+    AxisBits(const bool (&p)[3]) { set(p); }
+    FI void set(const bool (&p)[3]) {
+      MSET(0, p[X_AXIS], p[Y_AXIS], p[Z_AXIS], 0, 0, 0, 0, 0, 0);
+    }
+    FI AxisBits& operator=(const bool (&p)[3]) { set(p); return *this; }
+  #endif
+  #if LOGICAL_AXES > 4
+    AxisBits(const bool (&p)[4]) { set(p); }
+    FI void set(const bool (&p)[4]) {
+      MSET(0, p[X_AXIS], p[Y_AXIS], p[Z_AXIS], p[I_AXIS], 0, 0, 0, 0, 0);
+    }
+    FI AxisBits& operator=(const bool (&p)[4]) { set(p); return *this; }
+  #endif
+  #if LOGICAL_AXES > 5
+    AxisBits(const bool (&p)[5]) { set(p); }
+    FI void set(const bool (&p)[5]) {
+      MSET(0, p[X_AXIS], p[Y_AXIS], p[Z_AXIS], p[I_AXIS], p[J_AXIS], 0, 0, 0, 0);
+    }
+    FI AxisBits& operator=(const bool (&p)[5]) { set(p); return *this; }
+  #endif
+  #if LOGICAL_AXES > 6
+    AxisBits(const bool (&p)[6]) { set(p); }
+    FI void set(const bool (&p)[6]) {
+      MSET(0, p[X_AXIS], p[Y_AXIS], p[Z_AXIS], p[I_AXIS], p[J_AXIS], p[K_AXIS], 0, 0, 0);
+    }
+    FI AxisBits& operator=(const bool (&p)[6]) { set(p); return *this; }
+  #endif
+  #if LOGICAL_AXES > 7
+    AxisBits(const bool (&p)[7]) { set(p); }
+    FI void set(const bool (&p)[7]) {
+      MSET(0, p[X_AXIS], p[Y_AXIS], p[Z_AXIS], p[I_AXIS], p[J_AXIS], p[K_AXIS], p[U_AXIS], 0, 0);
+    }
+    FI AxisBits& operator=(const bool (&p)[7]) { set(p); return *this; }
+  #endif
+  #if LOGICAL_AXES > 8
+    AxisBits(const bool (&p)[8]) { set(p); }
+    FI void set(const bool (&p)[8]) {
+      MSET(0, p[X_AXIS], p[Y_AXIS], p[Z_AXIS], p[I_AXIS], p[J_AXIS], p[K_AXIS], p[U_AXIS], p[V_AXIS], 0);
+    }
+    FI AxisBits& operator=(const bool (&p)[8]) { set(p); return *this; }
+  #endif
+  #if LOGICAL_AXES > 9
+    AxisBits(const bool (&p)[9]) { set(p); }
+    FI void set(const bool (&p)[9]) {
+      MSET(0, p[X_AXIS], p[Y_AXIS], p[Z_AXIS], p[I_AXIS], p[J_AXIS], p[K_AXIS], p[U_AXIS], p[V_AXIS], p[W_AXIS]);
+    }
+    FI AxisBits& operator=(const bool (&p)[9]) { set(p); return *this; }
+  #endif
+  #undef MSET
+
+  FI const bool toggle(const AxisEnum n) { return TBI(bits, n); }
+
+  // Accessor via an AxisEnum (or any integer) [index]
+  FI const bool operator[](const int n) const { return TEST(bits, n); }
+  FI const bool operator[](const AxisEnum n) const { return TEST(bits, n); }
+
+  FI AxisBits& operator|=(const el &p) { bits |= el(p); return *this; }
+  FI AxisBits& operator&=(const el &p) { bits &= el(p); return *this; }
+  FI AxisBits& operator^=(const el &p) { bits ^= el(p); return *this; }
+
+  FI AxisBits& operator|=(const AxisBits &p) { bits |= p.bits; return *this; }
+  FI AxisBits& operator&=(const AxisBits &p) { bits &= p.bits; return *this; }
+  FI AxisBits& operator^=(const AxisBits &p) { bits ^= p.bits; return *this; }
+
+  FI bool operator==(const AxisBits &p) const { return p.bits == bits; }
+  FI bool operator!=(const AxisBits &p) const { return p.bits != bits; }
+
+  FI el operator|(const el &p) const { return bits | el(p); }
+  FI el operator&(const el &p) const { return bits & el(p); }
+  FI el operator^(const el &p) const { return bits ^ el(p); }
+
+  FI AxisBits operator|(const AxisBits &p) const { return AxisBits(bits | p.bits); }
+  FI AxisBits operator&(const AxisBits &p) const { return AxisBits(bits & p.bits); }
+  FI AxisBits operator^(const AxisBits &p) const { return AxisBits(bits ^ p.bits); }
+
+  FI operator bool() const { return !!bits; }
+  FI operator uint16_t() const { return uint16_t(bits & 0xFFFF); }
+  FI operator uint32_t() const { return uint32_t(bits); }
+
+};
+
 #undef _RECIP
 #undef _ABS
 #undef _LS
diff --git a/Marlin/src/feature/backlash.cpp b/Marlin/src/feature/backlash.cpp
index 13e2cd99eccb..256488762a7d 100644
--- a/Marlin/src/feature/backlash.cpp
+++ b/Marlin/src/feature/backlash.cpp
@@ -29,7 +29,7 @@
 #include "../module/motion.h"
 #include "../module/planner.h"
 
-axis_bits_t Backlash::last_direction_bits;
+AxisBits Backlash::last_direction_bits;
 xyz_long_t Backlash::residual_error{0};
 
 #ifdef BACKLASH_DISTANCE_MM
@@ -63,25 +63,25 @@ Backlash backlash;
  * spread over multiple segments, smoothing out artifacts even more.
  */
 
-void Backlash::add_correction_steps(const int32_t &da, const int32_t &db, const int32_t &dc, const axis_bits_t dm, block_t * const block) {
-  axis_bits_t changed_dir = last_direction_bits ^ dm;
+void Backlash::add_correction_steps(const int32_t &da, const int32_t &db, const int32_t &dc, const AxisBits dm, block_t * const block) {
+  AxisBits changed_dir = last_direction_bits ^ dm;
   // Ignore direction change unless steps are taken in that direction
   #if DISABLED(CORE_BACKLASH) || EITHER(MARKFORGED_XY, MARKFORGED_YX)
-    if (!da) CBI(changed_dir, X_AXIS);
-    if (!db) CBI(changed_dir, Y_AXIS);
-    if (!dc) CBI(changed_dir, Z_AXIS);
+    if (!da)        changed_dir.x = false;
+    if (!db)        changed_dir.y = false;
+    if (!dc)        changed_dir.z = false;
   #elif CORE_IS_XY
-    if (!(da + db)) CBI(changed_dir, X_AXIS);
-    if (!(da - db)) CBI(changed_dir, Y_AXIS);
-    if (!dc)        CBI(changed_dir, Z_AXIS);
+    if (!(da + db)) changed_dir.x = false;
+    if (!(da - db)) changed_dir.y = false;
+    if (!dc)        changed_dir.z = false;
   #elif CORE_IS_XZ
-    if (!(da + dc)) CBI(changed_dir, X_AXIS);
-    if (!(da - dc)) CBI(changed_dir, Z_AXIS);
-    if (!db)        CBI(changed_dir, Y_AXIS);
+    if (!(da + dc)) changed_dir.x = false;
+    if (!(da - dc)) changed_dir.z = false;
+    if (!db)        changed_dir.y = false;
   #elif CORE_IS_YZ
-    if (!(db + dc)) CBI(changed_dir, Y_AXIS);
-    if (!(db - dc)) CBI(changed_dir, Z_AXIS);
-    if (!da)        CBI(changed_dir, X_AXIS);
+    if (!(db + dc)) changed_dir.y = false;
+    if (!(db - dc)) changed_dir.z = false;
+    if (!da)        changed_dir.x = false;
   #endif
   last_direction_bits ^= changed_dir;
 
@@ -99,10 +99,10 @@ void Backlash::add_correction_steps(const int32_t &da, const int32_t &db, const
 
   LOOP_NUM_AXES(axis) {
     if (distance_mm[axis]) {
-      const bool reverse = TEST(dm, axis);
+      const bool reverse = dm[axis];
 
       // When an axis changes direction, add axis backlash to the residual error
-      if (TEST(changed_dir, axis))
+      if (changed_dir[axis])
         residual_error[axis] += (reverse ? -f_corr : f_corr) * distance_mm[axis] * planner.settings.axis_steps_per_mm[axis];
 
       // Decide how much of the residual error to correct in this segment
@@ -147,7 +147,7 @@ void Backlash::add_correction_steps(const int32_t &da, const int32_t &db, const
 int32_t Backlash::get_applied_steps(const AxisEnum axis) {
   if (axis >= NUM_AXES) return 0;
 
-  const bool reverse = TEST(last_direction_bits, axis);
+  const bool reverse = last_direction_bits[axis];
 
   const int32_t residual_error_axis = residual_error[axis];
 
diff --git a/Marlin/src/feature/backlash.h b/Marlin/src/feature/backlash.h
index 0bace526e53f..14c0fe20e378 100644
--- a/Marlin/src/feature/backlash.h
+++ b/Marlin/src/feature/backlash.h
@@ -29,7 +29,7 @@ class Backlash {
   static constexpr uint8_t all_on = 0xFF, all_off = 0x00;
 
 private:
-  static axis_bits_t last_direction_bits;
+  static AxisBits last_direction_bits;
   static xyz_long_t residual_error;
 
   #if ENABLED(BACKLASH_GCODE)
@@ -72,7 +72,7 @@ class Backlash {
     return has_measurement(X_AXIS) || has_measurement(Y_AXIS) || has_measurement(Z_AXIS);
   }
 
-  static void add_correction_steps(const int32_t &da, const int32_t &db, const int32_t &dc, const axis_bits_t dm, block_t * const block);
+  static void add_correction_steps(const int32_t &da, const int32_t &db, const int32_t &dc, const AxisBits dm, block_t * const block);
   static int32_t get_applied_steps(const AxisEnum axis);
 
   #if ENABLED(BACKLASH_GCODE)
diff --git a/Marlin/src/feature/runout.h b/Marlin/src/feature/runout.h
index fb4d0c269479..e6ca9f78cdd9 100644
--- a/Marlin/src/feature/runout.h
+++ b/Marlin/src/feature/runout.h
@@ -411,7 +411,7 @@ class FilamentSensorBase {
           // Only trigger on extrusion with XYZ movement to allow filament change and retract/recover.
           const uint8_t e = b->extruder;
           const int32_t steps = b->steps.e;
-          const float mm = (TEST(b->direction_bits, E_AXIS) ? -steps : steps) * planner.mm_per_step[E_AXIS_N(e)];
+          const float mm = (b->direction_bits.e ? -steps : steps) * planner.mm_per_step[E_AXIS_N(e)];
           if (e < NUM_RUNOUT_SENSORS) mm_countdown.runout[e] -= mm;
           #if ENABLED(FILAMENT_SWITCH_AND_MOTION)
             if (e < NUM_MOTION_SENSORS) mm_countdown.motion[e] -= mm;
diff --git a/Marlin/src/module/ft_motion.cpp b/Marlin/src/module/ft_motion.cpp
index dfef961c7968..2fe38025296d 100644
--- a/Marlin/src/module/ft_motion.cpp
+++ b/Marlin/src/module/ft_motion.cpp
@@ -484,33 +484,33 @@ void FxdTiCtrl::loadBlockData(block_t * const current_block) {
   const float totalLength = current_block->millimeters,
               oneOverLength = 1.0f / totalLength;
 
-  const axis_bits_t direction = current_block->direction_bits;
+  const AxisBits direction = current_block->direction_bits;
 
   #if HAS_X_AXIS
     x_startPosn = x_endPosn_prevBlock;
     float x_moveDist = current_block->steps.a / planner.settings.axis_steps_per_mm[X_AXIS];
-    if (TEST(direction, X_AXIS)) x_moveDist *= -1.0f;
+    if (direction.x) x_moveDist *= -1.0f;
     x_Ratio = x_moveDist * oneOverLength;
   #endif
 
   #if HAS_Y_AXIS
     y_startPosn = y_endPosn_prevBlock;
     float y_moveDist = current_block->steps.b / planner.settings.axis_steps_per_mm[Y_AXIS];
-    if (TEST(direction, Y_AXIS)) y_moveDist *= -1.0f;
+    if (direction.y) y_moveDist *= -1.0f;
     y_Ratio = y_moveDist * oneOverLength;
   #endif
 
   #if HAS_Z_AXIS
     z_startPosn = z_endPosn_prevBlock;
     float z_moveDist = current_block->steps.c / planner.settings.axis_steps_per_mm[Z_AXIS];
-    if (TEST(direction, Z_AXIS)) z_moveDist *= -1.0f;
+    if (direction.z) z_moveDist *= -1.0f;
     z_Ratio = z_moveDist * oneOverLength;
   #endif
 
   #if HAS_EXTRUDERS
     e_startPosn = e_endPosn_prevBlock;
     float extrusion = current_block->steps.e / planner.settings.axis_steps_per_mm[E_AXIS_N(current_block->extruder)];
-    if (TEST(direction, E_AXIS_N(current_block->extruder))) extrusion *= -1.0f;
+    if (direction.e) extrusion *= -1.0f;
     e_Ratio = extrusion * oneOverLength;
   #endif
 
diff --git a/Marlin/src/module/planner.cpp b/Marlin/src/module/planner.cpp
index ccf27a502ac1..52519b805c1b 100644
--- a/Marlin/src/module/planner.cpp
+++ b/Marlin/src/module/planner.cpp
@@ -1968,54 +1968,50 @@ bool Planner::_populate_block(
   #endif // PREVENT_COLD_EXTRUSION || PREVENT_LENGTHY_EXTRUDE
 
   // Compute direction bit-mask for this block
-  axis_bits_t dm = 0;
+  AxisBits dm;
   #if ANY(CORE_IS_XY, MARKFORGED_XY, MARKFORGED_YX)
-    if (da < 0) SBI(dm, X_HEAD);                  // Save the toolhead's true direction in X
-    if (db < 0) SBI(dm, Y_HEAD);                  // ...and Y
-    TERN_(HAS_Z_AXIS, if (dc < 0) SBI(dm, Z_AXIS));
+    dm.hx = (da < 0);                 // Save the toolhead's true direction in X
+    dm.hy = (db < 0);                 // ...and Y
+    TERN_(HAS_Z_AXIS, dm.z = (dc < 0));
   #endif
   #if IS_CORE
     #if CORE_IS_XY
-      if (da + db < 0) SBI(dm, A_AXIS);           // Motor A direction
-      if (CORESIGN(da - db) < 0) SBI(dm, B_AXIS); // Motor B direction
+      dm.a = (da + db < 0);           // Motor A direction
+      dm.b = (CORESIGN(da - db) < 0); // Motor B direction
     #elif CORE_IS_XZ
-      if (da < 0) SBI(dm, X_HEAD);                // Save the toolhead's true direction in X
-      if (db < 0) SBI(dm, Y_AXIS);
-      if (dc < 0) SBI(dm, Z_HEAD);                // ...and Z
-      if (da + dc < 0) SBI(dm, A_AXIS);           // Motor A direction
-      if (CORESIGN(da - dc) < 0) SBI(dm, C_AXIS); // Motor C direction
+      dm.hx = (da < 0);               // Save the toolhead's true direction in X
+      dm.y = (db < 0);
+      dm.hz = (dc < 0);               // ...and Z
+      dm.a = (da + dc < 0);           // Motor A direction
+      dm.c = (CORESIGN(da - dc) < 0); // Motor C direction
     #elif CORE_IS_YZ
-      if (da < 0) SBI(dm, X_AXIS);
-      if (db < 0) SBI(dm, Y_HEAD);                // Save the toolhead's true direction in Y
-      if (dc < 0) SBI(dm, Z_HEAD);                // ...and Z
-      if (db + dc < 0) SBI(dm, B_AXIS);           // Motor B direction
-      if (CORESIGN(db - dc) < 0) SBI(dm, C_AXIS); // Motor C direction
+      dm.x = (da < 0);
+      dm.hy = (db < 0);               // Save the toolhead's true direction in Y
+      dm.hz = (dc < 0);               // ...and Z
+      dm.b = (db + dc < 0);           // Motor B direction
+      dm.c = (CORESIGN(db - dc) < 0); // Motor C direction
     #endif
   #elif ENABLED(MARKFORGED_XY)
-    if (da + db < 0) SBI(dm, A_AXIS);              // Motor A direction
-    if (db < 0) SBI(dm, B_AXIS);                   // Motor B direction
+    dm.a = (da + db < 0);             // Motor A direction
+    dm.b = (db < 0);                  // Motor B direction
   #elif ENABLED(MARKFORGED_YX)
-    if (da < 0) SBI(dm, A_AXIS);                   // Motor A direction
-    if (db + da < 0) SBI(dm, B_AXIS);              // Motor B direction
+    dm.a = (da < 0);                  // Motor A direction
+    dm.b = (db + da < 0);             // Motor B direction
   #else
     XYZ_CODE(
-      if (da < 0) SBI(dm, X_AXIS),
-      if (db < 0) SBI(dm, Y_AXIS),
-      if (dc < 0) SBI(dm, Z_AXIS)
+      dm.x = (da < 0),
+      dm.y = (db < 0),
+      dm.z = (dc < 0)
     );
   #endif
 
   SECONDARY_AXIS_CODE(
-    if (di < 0) SBI(dm, I_AXIS),
-    if (dj < 0) SBI(dm, J_AXIS),
-    if (dk < 0) SBI(dm, K_AXIS),
-    if (du < 0) SBI(dm, U_AXIS),
-    if (dv < 0) SBI(dm, V_AXIS),
-    if (dw < 0) SBI(dm, W_AXIS)
+    dm.i = (di < 0), dm.j = (dj < 0), dm.k = (dk < 0),
+    dm.u = (du < 0), dm.v = (dv < 0), dm.w = (dw < 0)
   );
 
   #if HAS_EXTRUDERS
-    if (de < 0) SBI(dm, E_AXIS);
+    dm.e = (de < 0);
     const float esteps_float = de * e_factor[extruder];
     const uint32_t esteps = ABS(esteps_float) + 0.5f;
   #else
@@ -2435,11 +2431,11 @@ bool Planner::_populate_block(
 
   #ifdef XY_FREQUENCY_LIMIT
 
-    static axis_bits_t old_direction_bits; // = 0
+    static AxisBits old_direction_bits; // = 0
 
     if (xy_freq_limit_hz) {
       // Check and limit the xy direction change frequency
-      const axis_bits_t direction_change = block->direction_bits ^ old_direction_bits;
+      const AxisBits direction_change = block->direction_bits ^ old_direction_bits;
       old_direction_bits = block->direction_bits;
       segment_time_us = LROUND(float(segment_time_us) / speed_factor);
 
diff --git a/Marlin/src/module/planner.h b/Marlin/src/module/planner.h
index e072e94dbd86..eb0f072f4bf0 100644
--- a/Marlin/src/module/planner.h
+++ b/Marlin/src/module/planner.h
@@ -246,7 +246,7 @@ typedef struct PlannerBlock {
     uint32_t acceleration_rate;             // The acceleration rate used for acceleration calculation
   #endif
 
-  axis_bits_t direction_bits;               // Direction bits set for this block, where 1 is negative motion
+  AxisBits direction_bits;                  // Direction bits set for this block, where 1 is negative motion
 
   // Advance extrusion
   #if ENABLED(LIN_ADVANCE)
diff --git a/Marlin/src/module/stepper.cpp b/Marlin/src/module/stepper.cpp
index 8a4d801e7655..140a539ef36c 100644
--- a/Marlin/src/module/stepper.cpp
+++ b/Marlin/src/module/stepper.cpp
@@ -166,8 +166,8 @@ stepper_flags_t Stepper::axis_enabled; // {0}
 
 block_t* Stepper::current_block; // (= nullptr) A pointer to the block currently being traced
 
-axis_bits_t Stepper::last_direction_bits, // = 0
-            Stepper::axis_did_move; // = 0
+AxisBits Stepper::last_direction_bits, // = 0
+         Stepper::axis_did_move; // = 0
 
 bool Stepper::abort_current_block;
 
@@ -624,15 +624,11 @@ void Stepper::apply_directions() {
 
   DIR_WAIT_BEFORE();
 
-  TERN_(HAS_X_DIR, SET_STEP_DIR(X)); // A
-  TERN_(HAS_Y_DIR, SET_STEP_DIR(Y)); // B
-  TERN_(HAS_Z_DIR, SET_STEP_DIR(Z)); // C
-  TERN_(HAS_I_DIR, SET_STEP_DIR(I));
-  TERN_(HAS_J_DIR, SET_STEP_DIR(J));
-  TERN_(HAS_K_DIR, SET_STEP_DIR(K));
-  TERN_(HAS_U_DIR, SET_STEP_DIR(U));
-  TERN_(HAS_V_DIR, SET_STEP_DIR(V));
-  TERN_(HAS_W_DIR, SET_STEP_DIR(W));
+  NUM_AXIS_CODE(
+    SET_STEP_DIR(X), SET_STEP_DIR(Y), SET_STEP_DIR(Z), // ABC
+    SET_STEP_DIR(I), SET_STEP_DIR(J), SET_STEP_DIR(K),
+    SET_STEP_DIR(U), SET_STEP_DIR(V), SET_STEP_DIR(W)
+  );
 
   #if HAS_EXTRUDERS
      // Because this is valid for the whole block we don't know
@@ -1829,7 +1825,7 @@ void Stepper::pulse_phase_isr() {
         de += step_fwd ? -128 : 128; \
         if ((MAXDIR(AXIS) && step_bak) || (MINDIR(AXIS) && step_fwd)) { \
           { USING_TIMED_PULSE(); START_TIMED_PULSE(); AWAIT_LOW_PULSE(); } \
-          TBI(last_direction_bits, _AXIS(AXIS)); \
+          last_direction_bits.toggle(_AXIS(AXIS)); \
           DIR_WAIT_BEFORE(); \
           SET_STEP_DIR(AXIS); \
           DIR_WAIT_AFTER(); \
@@ -1861,11 +1857,11 @@ void Stepper::pulse_phase_isr() {
 
         #if STEPPER_PAGE_FORMAT == SP_4x4D_128
 
-          #define PAGE_SEGMENT_UPDATE(AXIS, VALUE) do{   \
-                 if ((VALUE) <  7) SBI(dm, _AXIS(AXIS)); \
-            else if ((VALUE) >  7) CBI(dm, _AXIS(AXIS)); \
-            page_step_state.sd[_AXIS(AXIS)] = VALUE;     \
-            page_step_state.bd[_AXIS(AXIS)] += VALUE;    \
+          #define PAGE_SEGMENT_UPDATE(AXIS, VALUE) do{      \
+                 if ((VALUE) <  7) dm[_AXIS(AXIS)] = true;  \
+            else if ((VALUE) >  7) dm[_AXIS(AXIS)] = false; \
+            page_step_state.sd[_AXIS(AXIS)] = VALUE;        \
+            page_step_state.bd[_AXIS(AXIS)] += VALUE;       \
           }while(0)
 
           #define PAGE_PULSE_PREP(AXIS) do{ \
@@ -1881,7 +1877,7 @@ void Stepper::pulse_phase_isr() {
             case 0: {
               const uint8_t low = page_step_state.page[page_step_state.segment_idx],
                            high = page_step_state.page[page_step_state.segment_idx + 1];
-              axis_bits_t dm = last_direction_bits;
+              const AxisBits dm = last_direction_bits;
 
               PAGE_SEGMENT_UPDATE(X, low >> 4);
               PAGE_SEGMENT_UPDATE(Y, low & 0xF);
@@ -2417,7 +2413,7 @@ hal_timer_t Stepper::block_phase_isr() {
               la_interval = calc_timer_interval((reverse_e ? la_step_rate - step_rate : step_rate - la_step_rate) >> current_block->la_scaling);
 
               if (reverse_e != motor_direction(E_AXIS)) {
-                TBI(last_direction_bits, E_AXIS);
+                last_direction_bits.toggle(E_AXIS);
                 count_direction.e = -count_direction.e;
 
                 DIR_WAIT_BEFORE();
@@ -2648,7 +2644,7 @@ hal_timer_t Stepper::block_phase_isr() {
         #define Z_MOVE_TEST !!current_block->steps.c
       #endif
 
-      axis_bits_t axis_bits = 0;
+      AxisBits axis_bits;
       NUM_AXIS_CODE(
         if (X_MOVE_TEST)            SBI(axis_bits, A_AXIS),
         if (Y_MOVE_TEST)            SBI(axis_bits, B_AXIS),
@@ -2692,24 +2688,24 @@ hal_timer_t Stepper::block_phase_isr() {
 
       #if ENABLED(INPUT_SHAPING_X)
         if (shaping_x.enabled) {
-          const int64_t steps = TEST(current_block->direction_bits, X_AXIS) ? -int64_t(current_block->steps.x) : int64_t(current_block->steps.x);
+          const int64_t steps = current_block->direction_bits.x ? -int64_t(current_block->steps.x) : int64_t(current_block->steps.x);
           shaping_x.last_block_end_pos += steps;
 
           // If there are any remaining echos unprocessed, then direction change must
           // be delayed and processed in PULSE_PREP_SHAPING. This will cause half a step
           // to be missed, which will need recovering and this can be done through shaping_x.remainder.
-          shaping_x.forward = !TEST(current_block->direction_bits, X_AXIS);
-          if (!ShapingQueue::empty_x()) SET_BIT_TO(current_block->direction_bits, X_AXIS, TEST(last_direction_bits, X_AXIS));
+          shaping_x.forward = !current_block->direction_bits.x;
+          if (!ShapingQueue::empty_x()) current_block->direction_bits.x = last_direction_bits.x;
         }
       #endif
 
       // Y follows the same logic as X (but the comments aren't repeated)
       #if ENABLED(INPUT_SHAPING_Y)
         if (shaping_y.enabled) {
-          const int64_t steps = TEST(current_block->direction_bits, Y_AXIS) ? -int64_t(current_block->steps.y) : int64_t(current_block->steps.y);
+          const int64_t steps = current_block->direction_bits.y ? -int64_t(current_block->steps.y) : int64_t(current_block->steps.y);
           shaping_y.last_block_end_pos += steps;
-          shaping_y.forward = !TEST(current_block->direction_bits, Y_AXIS);
-          if (!ShapingQueue::empty_y()) SET_BIT_TO(current_block->direction_bits, Y_AXIS, TEST(last_direction_bits, Y_AXIS));
+          shaping_y.forward = !current_block->direction_bits.y;
+          if (!ShapingQueue::empty_y()) current_block->direction_bits.y = last_direction_bits.y;
         }
       #endif
 
@@ -2912,24 +2908,10 @@ void Stepper::init() {
       Z4_DIR_INIT();
     #endif
   #endif
-  #if HAS_I_DIR
-    I_DIR_INIT();
-  #endif
-  #if HAS_J_DIR
-    J_DIR_INIT();
-  #endif
-  #if HAS_K_DIR
-    K_DIR_INIT();
-  #endif
-  #if HAS_U_DIR
-    U_DIR_INIT();
-  #endif
-  #if HAS_V_DIR
-    V_DIR_INIT();
-  #endif
-  #if HAS_W_DIR
-    W_DIR_INIT();
-  #endif
+  SECONDARY_AXIS_CODE(
+    I_DIR_INIT(), J_DIR_INIT(), K_DIR_INIT(),
+    U_DIR_INIT(), V_DIR_INIT(), W_DIR_INIT()
+  );
   #if HAS_E0_DIR
     E0_DIR_INIT();
   #endif
diff --git a/Marlin/src/module/stepper.h b/Marlin/src/module/stepper.h
index 532db65dd93c..9e45ffa45f6e 100644
--- a/Marlin/src/module/stepper.h
+++ b/Marlin/src/module/stepper.h
@@ -317,17 +317,17 @@ class Stepper {
     #endif
 
     #if ENABLED(FREEZE_FEATURE)
-      static bool frozen;                   // Set this flag to instantly freeze motion
+      static bool frozen;                 // Set this flag to instantly freeze motion
     #endif
 
   private:
 
-    static block_t* current_block;          // A pointer to the block currently being traced
+    static block_t* current_block;        // A pointer to the block currently being traced
 
-    static axis_bits_t last_direction_bits, // The next stepping-bits to be output
-                       axis_did_move;       // Last Movement in the given direction is not null, as computed when the last movement was fetched from planner
+    static AxisBits last_direction_bits,  // The next stepping-bits to be output
+                    axis_did_move;        // Last Movement in the given direction is not null, as computed when the last movement was fetched from planner
 
-    static bool abort_current_block;        // Signals to the stepper that current block should be aborted
+    static bool abort_current_block;      // Signals to the stepper that current block should be aborted
 
     #if ENABLED(X_DUAL_ENDSTOPS)
       static bool locked_X_motor, locked_X2_motor;
@@ -523,10 +523,10 @@ class Stepper {
     FORCE_INLINE static void quick_stop() { abort_current_block = true; }
 
     // The direction of a single motor. A true result indicates reversed or negative motion.
-    FORCE_INLINE static bool motor_direction(const AxisEnum axis) { return TEST(last_direction_bits, axis); }
+    FORCE_INLINE static bool motor_direction(const AxisEnum axis) { return last_direction_bits[axis]; }
 
     // The last movement direction was not null on the specified axis. Note that motor direction is not necessarily the same.
-    FORCE_INLINE static bool axis_is_moving(const AxisEnum axis) { return TEST(axis_did_move, axis); }
+    FORCE_INLINE static bool axis_is_moving(const AxisEnum axis) { return axis_did_move[axis]; }
 
     // Handle a triggered endstop
     static void endstop_triggered(const AxisEnum axis);
@@ -626,7 +626,7 @@ class Stepper {
     static void apply_directions();
 
     // Set direction bits and update all stepper DIR states
-    static void set_directions(const axis_bits_t bits) {
+    static void set_directions(const AxisBits bits) {
       last_direction_bits = bits;
       apply_directions();
     }