Add dynamic 3-point bed-leveling support

Adds G29 commands to register bed level points.  When three points
are registered, the plane of the bed is calculated and dynamic bed
leveling takes effect.

Add a warning if bed-leveling is enabled when MAX_JERK_Z is zero.
In this case lookahead will always fail when bed-leveling is active
since the Z-axis is not allowed to move during lookahead.
This commit is contained in:
Phil Hord 2018-07-04 16:06:56 -07:00
parent 47dfcf1a44
commit 77d8583cca
18 changed files with 563 additions and 41 deletions

199
bed_leveling.c Normal file
View File

@ -0,0 +1,199 @@
/** \file
\brief Dynamic Z-height compensation for out-of-level print bed.
*/
#include "bed_leveling.h"
#ifdef BED_LEVELING
#include <stdint.h>
#include <stdlib.h>
#include "dda_maths.h"
#include "debug.h"
#include "sersendf.h"
#include "config_wrapper.h"
struct point {
int32_t x, y, z;
};
// Accept exactly three points for bed leveling
static uint8_t level_index = 0;
static struct point bed_level_map[3];
// Alias the three points
static struct point const * const P = &bed_level_map[0];
static struct point const * const Q = &bed_level_map[1];
static struct point const * const R = &bed_level_map[2];
int bed_plane_calculate(void);
// Reset the bed level values to "unknown"
void bed_level_reset() {
level_index = 0;
}
// Scale x and y down to tenths of mm to keep math from overflowing
#define SCALE 100
static int32_t scale_to_dum(int32_t a) {
if (a<0) return (a-SCALE/2)/SCALE;
return (a+SCALE/2)/SCALE;
}
// Register a point on the bed plane; coordinates in um
void bed_level_register(int32_t x, int32_t y, int32_t z) {
// Scale x and y to tenths of mm; keep z in um
x = scale_to_dum(x);
y = scale_to_dum(y);
// Find a previous registered point at the same location or use a new location
struct point * p = bed_level_map;
int i = 0;
for (; i < level_index; i++, p++) {
if (p->x == x && p->y == y)
break;
}
// We can only handle three points
if (i >= 3)
return;
p->x = x;
p->y = y;
p->z = z;
// Bump the index if we just used a new location
if (i == level_index)
++level_index;
// Nothing more to do until we have three points
if (level_index < 3)
return;
// We have three points. Try to calculate the plane of the bed.
if (!bed_plane_calculate())
--level_index;
}
// Pre-scaled coefficients of the planar equation,
// Ax + By + Cz + K= 0
//
// When we have these coefficients, we're only going to use them relative to -1/C, so
// Ac = -A/C; Bc = -B/C; Kc = 0 (because we translate a point to the origin)
// f(x,y) = z = Ac*x + Bc*y + Kc
static uint32_t Aq, Ar, Bq, Br, C;
static int8_t Asign, Bsign;
int bed_leveling_active() {
// No adjustment if not calibrated yet
return level_index == 3;
}
void bed_level_report() {
sersendf_P(PSTR("Bed leveling status:"));
if (!bed_leveling_active()) {
sersendf_P(PSTR(" not"));
}
sersendf_P(PSTR(" active (%d) positions registered\n"), level_index);
for (int i = 0; i < level_index; i++) {
sersendf_P(PSTR(" %d: G29 S1 X%lq Y%lq Z%lq\n"),
i+1, bed_level_map[i].x * SCALE, bed_level_map[i].y * SCALE, bed_level_map[i].z);
}
}
int32_t bed_level_adjustment(int32_t x, int32_t y) {
int32_t za, zb;
// No adjustment if not calibrated yet
if (!bed_leveling_active())
return 0;
x = scale_to_dum(x);
y = scale_to_dum(y);
x -= P->x;
y -= P->y;
za = muldivQR(x, Aq, Ar, C);
if (Asign)
za = -za;
zb = muldivQR(y, Bq, Br, C);
if (Bsign)
zb = -zb;
return P->z - za - zb;
}
int bed_plane_calculate() {
// Coefficients of the planar equation,
// Ax + By + Cz + K = 0
int32_t a, b, c;
// Draw two vectors on the plane, u = B-A and v = C-A
int32_t Ui, Uj, Uk, Vi, Vj, Vk;
// U = vector(QP)
Ui = Q->x - P->x;
Uj = Q->y - P->y;
Uk = Q->z - P->z;
// V = vector(RP)
Vi = R->x - P->x;
Vj = R->y - P->y;
Vk = R->z - P->z;
// Find normal vector (a,b,c) = (U x V) and is perpendicular to the plane
a = Uj*Vk - Uk*Vj;
b = Uk*Vi - Ui*Vk;
c = Ui*Vj - Uj*Vi;
// Notes:
// * Ignore K (constant) by translating plane to pass through origin at P (0,0,0)
// * if a==0 and b==0, the bed is already level; z-offset is still important
// * if c==0 the bed is perpendicular or the points are colinear
if (c == 0)
return 0;
if (DEBUG_DDA && (debug_flags & DEBUG_DDA))
sersendf_P(PSTR("Coefficients: A:%ld B:%ld C:%ld\n"), a, b, c);
// muldiv requires positive parameters
// remember the signs separately
Asign = a < 0;
Bsign = b < 0;
if (Asign) a = -a;
if (Bsign) b = -b;
// Combine A/C and B/C, so combine their signs, too
if (c < 0) {
c = -c;
Asign = !Asign;
Bsign = !Bsign;
}
// Pre-calc the coefficients A/C and B/C
Aq = a / c;
Ar = a % c;
Bq = b / c;
Br = b % c;
C = c;
int ret = 1;
// Sanity check
for (int i = 0; i < level_index; i++) {
int32_t x=bed_level_map[i].x * SCALE, y=bed_level_map[i].y * SCALE;
int32_t validation = bed_level_adjustment(x, y);
if (labs(validation - bed_level_map[i].z) > 10) {
sersendf_P(PSTR("!! Bed plane validation failed: Point %d: X:%lq Y:%lq Expected Z:%lq Calculated Z:%lq\n"),
i+1, x, y, bed_level_map[i].z, validation);
ret = 0; // invalidate results on error
}
}
return ret;
}
#endif /* BED_LEVELING */

42
bed_leveling.h Normal file
View File

@ -0,0 +1,42 @@
#ifndef _BED_LEVELING_H
#define _BED_LEVELING_H
#include "config_wrapper.h"
#include <stdint.h>
#include "dda.h"
#ifdef BED_LEVELING
// Clears all registered points and disables dynamic leveling
void bed_level_reset(void);
// Returns true if bed leveling a plane is mapped and leveling is active
int bed_leveling_active(void);
// Report information about bed leveling calculations
void bed_level_report(void);
// Read the z-adjustment for the given x,y position
int32_t bed_level_adjustment(int32_t x, int32_t y);
// Register a point as being "on the bed plane". Three points are required
// to define a plane. After three non-colinear points are registered, the
// adjustment is active and can be read from bed_level_adjustment.
// Note: units for x, y and z are um but the three (X, Y) points should be
// distinct enough in mm to define an accurate plane.
void bed_level_register(int32_t x, int32_t y, int32_t z);
#endif /* BED_LEVELING */
static int32_t bed_level_offset(const axes_int32_t) __attribute__ ((always_inline));
inline int32_t bed_level_offset(const axes_int32_t axis) {
#ifdef BED_LEVELING
return bed_level_adjustment(axis[X], axis[Y]);
#else
return 0;
#endif
}
#endif

View File

@ -184,9 +184,17 @@ DEFINE_HOMING(x_negative, y_negative, z_negative, none)
*/
#define MAX_JERK_X 200
#define MAX_JERK_Y 200
#define MAX_JERK_Z 0
#define MAX_JERK_Z 20
#define MAX_JERK_E 200
/** \def BED_LEVELING
Define this to enable dynamic bed leveling using the G29 command and
3-point planar bed mapping. Allows the printer to compensate dynamically
for a print bed which is flat but is not quite level.
Enabling bed-leveling requires about 2400 bytes of flash memory.
*/
//#define BED_LEVELING
/***************************************************************************\
* *

View File

@ -184,9 +184,17 @@ DEFINE_HOMING(x_negative, y_negative, z_negative, none)
*/
#define MAX_JERK_X 300
#define MAX_JERK_Y 300
#define MAX_JERK_Z 0
#define MAX_JERK_Z 20
#define MAX_JERK_E 300
/** \def BED_LEVELING
Define this to enable dynamic bed leveling using the G29 command and
3-point planar bed mapping. Allows the printer to compensate dynamically
for a print bed which is flat but is not quite level.
Enabling bed-leveling requires about 2400 bytes of flash memory.
*/
//#define BED_LEVELING
/***************************************************************************\
* *

View File

@ -184,9 +184,17 @@ DEFINE_HOMING(z_negative, x_negative, y_negative, none)
*/
#define MAX_JERK_X 100
#define MAX_JERK_Y 100
#define MAX_JERK_Z 0
#define MAX_JERK_Z 2
#define MAX_JERK_E 200
/** \def BED_LEVELING
Define this to enable dynamic bed leveling using the G29 command and
3-point planar bed mapping. Allows the printer to compensate dynamically
for a print bed which is flat but is not quite level.
Enabling bed-leveling requires about 2400 bytes of flash memory.
*/
//#define BED_LEVELING
/***************************************************************************\
* *

View File

@ -184,9 +184,17 @@ DEFINE_HOMING(x_negative, y_negative, z_negative, none)
*/
#define MAX_JERK_X 200
#define MAX_JERK_Y 200
#define MAX_JERK_Z 0
#define MAX_JERK_Z 20
#define MAX_JERK_E 200
/** \def BED_LEVELING
Define this to enable dynamic bed leveling using the G29 command and
3-point planar bed mapping. Allows the printer to compensate dynamically
for a print bed which is flat but is not quite level.
Enabling bed-leveling requires about 2400 bytes of flash memory.
*/
//#define BED_LEVELING
/***************************************************************************\
* *

View File

@ -184,9 +184,17 @@ DEFINE_HOMING(x_negative, y_negative, z_negative, none)
*/
#define MAX_JERK_X 20
#define MAX_JERK_Y 20
#define MAX_JERK_Z 0
#define MAX_JERK_Z 2
#define MAX_JERK_E 200
/** \def BED_LEVELING
Define this to enable dynamic bed leveling using the G29 command and
3-point planar bed mapping. Allows the printer to compensate dynamically
for a print bed which is flat but is not quite level.
Enabling bed-leveling requires about 2400 bytes of flash memory.
*/
//#define BED_LEVELING
/***************************************************************************\
* *

View File

@ -85,6 +85,15 @@
#undef LOOKAHEAD
#endif
/**
LOOKAHEAD won't work if Z-jerk is zero and bed leveling is active
because most moves will have Z-steps and lookahead will be skipped.
*/
#if defined BED_LEVELING && defined LOOKAHEAD && MAX_JERK_Z==0
#warning When bed-leveling is activated, lookahead will be ineffective \
because MAX_JERK_Z is zero.
#endif
/**
Silently discard EECONFIG on ARM. Silently to not disturb regression tests.

View File

@ -13,6 +13,7 @@ class MiscellaneousPage(wx.Panel, Page):
self.font = font
self.labels = {'USE_INTERNAL_PULLUPS': "Use Internal Pullups",
'BED_LEVELING': "Enable dynamic bed leveling",
'Z_AUTODISABLE': "Z Autodisable",
'EECONFIG': "Enable EEPROM Storage",
'BANG_BANG': "Enable",
@ -79,6 +80,10 @@ class MiscellaneousPage(wx.Panel, Page):
cb = self.addCheckBox(k, self.onCheckBox)
sz.Add(cb, pos = (7, 1))
k = 'BED_LEVELING'
cb = self.addCheckBox(k, self.onCheckBox)
sz.Add(cb, pos = (8, 1))
b = wx.StaticBox(self, wx.ID_ANY, "BANG BANG Bed Control")
b.SetFont(font)
sbox = wx.StaticBoxSizer(b, wx.VERTICAL)

View File

@ -184,9 +184,17 @@ DEFINE_HOMING(x_negative, y_negative, z_negative, none)
*/
#define MAX_JERK_X 20
#define MAX_JERK_Y 20
#define MAX_JERK_Z 0
#define MAX_JERK_Z 5
#define MAX_JERK_E 200
/** \def BED_LEVELING
Define this to enable dynamic bed leveling using the G29 command and
3-point planar bed mapping. Allows the printer to compensate dynamically
for a print bed which is flat but is not quite level.
Enabling bed-leveling requires about 2400 bytes of flash memory.
*/
#define BED_LEVELING
/***************************************************************************\
* *

24
dda.c
View File

@ -21,6 +21,7 @@
#include "pinio.h"
#include "memory_barrier.h"
#include "home.h"
#include "bed_leveling.h"
//#include "graycode.c"
#ifdef DC_EXTRUDER
@ -112,7 +113,13 @@ void dda_init(void) {
This is needed for example after homing or a G92. The new location must be in startpoint already.
*/
void dda_new_startpoint(void) {
void dda_new_startpoint() {
if (DEBUG_DDA && (debug_flags & DEBUG_DDA)) {
int32_t z_offset = bed_level_offset(startpoint.axis);
sersendf_P(PSTR("\nReset: X %lq Y %lq Z %lq Zofs %lq F %lu\n"),
startpoint.axis[X], startpoint.axis[Y],
startpoint.axis[Z], z_offset, startpoint.F);
}
axes_um_to_steps(startpoint.axis, startpoint_steps.axis);
startpoint_steps.axis[E] = um_to_steps(startpoint.axis[E], E);
}
@ -943,22 +950,12 @@ void update_current_position() {
DDA *dda = mb_tail_dda;
enum axis_e i;
static const axes_uint32_t PROGMEM steps_per_m_P = {
STEPS_PER_M_X,
STEPS_PER_M_Y,
STEPS_PER_M_Z,
STEPS_PER_M_E
};
if (dda != NULL) {
uint32_t axis_um;
axes_int32_t delta_um;
for (i = X; i < AXIS_COUNT; i++) {
axis_um = muldiv(move_state.steps[i],
1000000,
pgm_read_dword(&steps_per_m_P[i]));
axis_um = steps_to_um(move_state.steps[i], i);
delta_um[i] = (int32_t)get_direction(dda, i) * axis_um;
}
@ -968,6 +965,9 @@ void update_current_position() {
current_position.axis[i] = dda->endpoint.axis[i] - delta_um[i];
}
// Compensate for bed-leveling z-offset
current_position.axis[Z] -= bed_level_offset(current_position.axis);
current_position.F = dda->endpoint.F;
}
else {

View File

@ -6,28 +6,16 @@
#include <stdlib.h>
#include "dda_maths.h"
#include "bed_leveling.h"
void
carthesian_to_carthesian(const TARGET *startpoint, const TARGET *target,
axes_uint32_t delta_um, axes_int32_t steps) {
enum axis_e i;
for (i = X; i < E; i++) {
delta_um[i] = (uint32_t)labs(target->axis[i] - startpoint->axis[i]);
steps[i] = um_to_steps(target->axis[i], i);
}
/* Replacing the above five lines with this costs about 200 bytes binary
size on AVR, but also takes about 120 clock cycles less during movement
preparation. The smaller version was kept for our Arduino Nano friends.
delta_um[X] = (uint32_t)labs(target->axis[X] - startpoint->axis[X]);
steps[X] = um_to_steps(target->axis[X], X);
delta_um[Y] = (uint32_t)labs(target->axis[Y] - startpoint->axis[Y]);
steps[Y] = um_to_steps(target->axis[Y], Y);
delta_um[Z] = (uint32_t)labs(target->axis[Z] - startpoint->axis[Z]);
steps[Z] = um_to_steps(target->axis[Z], Z);
*/
axes_um_to_steps_cartesian(target->axis, steps);
}
void
@ -43,17 +31,15 @@ carthesian_to_corexy(const TARGET *startpoint, const TARGET *target,
}
void axes_um_to_steps_cartesian(const axes_int32_t um, axes_int32_t steps) {
enum axis_e i;
for (i = X; i < E; i++) {
steps[i] = um_to_steps(um[i], i);
}
steps[X] = um_to_steps(um[X], X);
steps[Y] = um_to_steps(um[Y], Y);
steps[Z] = um_to_steps(um[Z] + bed_level_offset(um), Z);
}
void axes_um_to_steps_corexy(const axes_int32_t um, axes_int32_t steps) {
steps[X] = um_to_steps(um[X] + um[Y], X);
steps[Y] = um_to_steps(um[X] - um[Y], Y);
steps[Z] = um_to_steps(um[Z], Z);
steps[Z] = um_to_steps(um[Z] + bed_level_offset(um), Z);
}
void delta_to_axes_cartesian(axes_int32_t delta) {

View File

@ -28,6 +28,13 @@ const axes_uint32_t PROGMEM axis_qr_P = {
(uint32_t)STEPS_PER_M_E % UM_PER_METER
};
const axes_uint32_t PROGMEM steps_per_m_P = {
(uint32_t)STEPS_PER_M_X,
(uint32_t)STEPS_PER_M_Y,
(uint32_t)STEPS_PER_M_Z,
(uint32_t)STEPS_PER_M_E
};
/*!
Integer multiply-divide algorithm. Returns the same as muldiv(multiplicand, multiplier, divisor), but also allowing to use precalculated quotients and remainders.

View File

@ -34,6 +34,13 @@ inline int32_t um_to_steps(int32_t distance, enum axis_e a) {
pgm_read_dword(&axis_qr_P[a]), UM_PER_METER);
}
extern const axes_uint32_t steps_per_m_P;
static int32_t steps_to_um(int32_t, enum axis_e) __attribute__ ((always_inline));
inline int32_t steps_to_um(int32_t steps, enum axis_e a) {
return muldiv(steps, UM_PER_METER, pgm_read_dword(&steps_per_m_P[a]));
}
// approximate 2D distance
uint32_t approx_distance(uint32_t dx, uint32_t dy);

View File

@ -24,7 +24,7 @@
#include "config_wrapper.h"
#include "home.h"
#include "sd.h"
#include "bed_leveling.h"
/// the current tool
uint8_t tool;
@ -224,6 +224,62 @@ void process_gcode_command() {
}
break;
#ifdef BED_LEVELING
case 29:
//? --- G29: Bed leveling registration ---
//?
//? Example: G29 S1
//?
//? Registers the Z-offset for a specific point on the print bed.
//? In this case the current position is used as the registration
//? point, but a different position can be specified by including
//? the X, Y and Z coordinate values.
//?
//? Three points must be registered before the dynamic bed leveling
//? feature is activated. Once three points are registered, the bed
//? is mapped assuming a flat plane and Z-offsets are adjusted
//? automatically during movements to follow the mapped plane. The
//? adjusted position is not displayed to the client, for example
//? in M114 results.
//?
//? The S value controls the action as follows:
//? S0 displays the current bed leveling status
//? S1 registers a new point on the 3-point plane mapping
//? S5 clears all registered points and disables dynamic leveling
//?
//? G29 S1 X100 Y50 Z-0.3
//?
//? This command registers the specific point 100,50 => -0.3
//?
//? G29 S1
//?
//? This command registers the current head position as a point in
//? the plane map.
//?
queue_wait();
if (next_target.seen_S) {
switch (next_target.S) {
case 5: // reset bed leveling registration points
bed_level_reset();
break;
case 1: // Register a new registration point
bed_level_register(next_target.target.axis[X], next_target.target.axis[Y], next_target.target.axis[Z]);
break;
case 0: // Report leveling status
bed_level_report();
break;
}
}
// Restore position, ignoring any axes included in G29 cmd
next_target.target = startpoint;
break;
#endif /* BED_LEVELING */
case 90:
//? --- G90: Set to Absolute Positioning ---
//?

12
home.c
View File

@ -1,5 +1,5 @@
#include "home.h"
#include "bed_leveling.h"
/** \file
\brief Homing routines
*/
@ -159,6 +159,12 @@ void home_axis(enum axis_e n, int8_t dir, enum axis_endstop_e endstop_check) {
queue_wait();
set_axis_home_position(n, dir);
dda_new_startpoint();
#ifdef BED_LEVELING
// Move to calculated Z-plane offset
if (n==Z)
enqueue(&next_target.target);
#endif /* BED_LEVELING */
}
void set_axis_home_position(enum axis_e n, int8_t dir) {
@ -198,4 +204,8 @@ void set_axis_home_position(enum axis_e n, int8_t dir) {
}
}
startpoint.axis[n] = next_target.target.axis[n] = home_position;
if (n == Z) {
// Compensate for z-offset that will be added in by next move
startpoint.axis[n] -= bed_level_offset(startpoint.axis);
}
}

107
research/planes.py Normal file
View File

@ -0,0 +1,107 @@
#!/usr/bin/env python
# Experiments with coefficients of a geometric plane
# Resources:
# http://www.wolframalpha.com/input/?i=plane+through+(1,-2,0),(4,-2,-2),(4,1,4)&lk=3
# ==> 2 x - 6 y + 3 z + 14 == 0
# Translate a point relative to some origin
def translate(point, origin):
return tuple([a-b for a,b in zip(point, origin)])
# Given two points in 3d space, define a vector
def vector(p1, p2):
return tuple([b-a for a,b in zip(p1,p2)])
# Given two vectors in a plane, find the normal vector
def normal(u, v):
# A normal vector is the cross-product of two coplanar vectors
return tuple([
u[1]*v[2] - u[2]*v[1],
u[2]*v[0] - u[0]*v[2],
u[0]*v[1] - u[1]*v[0]
])
def plane_from_three_points(P, Q, R):
u = vector(P, Q)
v = vector(P, R)
n = normal(u, v)
# Find the coefficients
(A,B,C) = n
# The equation of the plane is thus Ax+By+Cz+K=0.
# Solve for K to get the final coefficient
(x,y,z) = P
K = -(A*x + B*y + C*z)
return (A, B, C, K)
# find the Z offset for any x,y
# z = -(Ax + By + K) / C
def calcz(x, y, plane, translation=(0,0,0)):
(A,B,C,K) = plane
(tx, ty, tz) = translation
return -(A*(x-tx) + B*(y-ty) + K) / C + tz
# Verify a point is on this plane
def validate(plane, point):
(A, B, C, K) = plane
(x, y, z) = point
return z == calcz(x, y, plane)
def verify_plane(points):
print ' ', '\n '.join([str(p) for p in points])
plane = plane_from_three_points( *points)
print 'Plane coordinates: ', plane
if plane[2] == 0:
print ' Error: points are colinear'
return
valid = True
for p in points:
if not validate(plane, p):
print "Failed: sample point not on plane, ", p
valid = False
print "Validation:", "Failed" if not valid else "Passed"
samples = [
# canonical example
[ (1,-2,0), (4,-2,-2), (4,1,4) ],
# three colinear points (infinite planes)
[ (2,2,2), (4,4,4), (10,10,10) ],
# Extreme tilt example in mm
[ (57,123,-5), (200,0,35), (0,207,2) ],
# Some more examples in um
[ (0, 0, 1300), (200000, 200000, 3500), (0, 150000, -1000) ],
[ (20000, 20000, -300), (220000, 120000, -1700), (120000, 220000, -700) ],
# some example in tenths of mm
[ (200, 200, -300), (2200, 1200, -1700), (1200, 2200, -700) ],
[ (20000, 20000 , -300 ), (220000, 120000 , -1700 ), (120000, 220000 , -700 ) ],
[ (200, 200, -300 ), (2200, 1200, -1700 ), (1200, 2200, -700 ) ]
]
for points in samples:
verify_plane(points)
print "====[Translated]========="
# Translate plane to origin at P (simplifies by removing K coefficient)
# A*x' + B*y' + C*z' = 0
P = points[0]
T = translate((0,0,0), P)
xpoints = [translate(p, P) for p in points]
verify_plane(xpoints)
print "=========================\n"

View File

@ -0,0 +1,46 @@
(Test bed leveling)
m111 s32
G28
G29 S5
G29 S1 X20.000 Y20.000 Z-2.000
G29 S1 X220.000 Y120.000 Z-1.700
G29 S1 X120.000 Y220.000 Z-0.700
g29 s0
m114
g1 x100 y100
m114
G28
M114
G4 ; dwell
G1 x200 y0
G4 ; dwell
m114
G1 x199
G4 ; dwell
G1 x198
G4 ; dwell
G1 x197
G4 ; dwell
G1 x196
G4 ; dwell
G1 x195
G4 ; dwell
G1 x194
G4 ; dwell
g28
M114
G1 x1 y1
G4 ; dwell
m114
G28
G29 S5
G29 S1 X20.000 Y20.000 Z-0.300
G29 S1 X220.000 Y120.000 Z-1.700
G29 S1 X120.000 Y220.000 Z-0.700
g29 s0